目录
一、memcpy库函数介绍
1. memcpy的使用
memcpy这个库函数用于是任何类型,将一个地址的内容复制到到另一个地址上,它是针对于内存的修改,使用使用过程中是memcpy(arr1(拷贝地址),arr2(从这里抄内容到arr1中),size_t(字节个数)),需要头文件string.h,返回的是arr1这个已经拷贝的地址。
它遇到\0不会停止,它只受字节个数的影响,它针对的是内存的修改,也就是一个一个字节的修改。注意这里是适用于不重叠的内存。
2. memcpy的模拟
根据以上的信息,可以知道memcpy的修改是通过一个一个字节的修改而以小改大。
#include<stdio.h>
void* my_memcpy(void* dest, const void* src, size_t num)
{
char* ret = dest;//记入起始地址
//拷贝
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;//void*类型不能改变值,需要强制类型转换
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[20];
int arr2[] = { 0,1,2,3,4,5,6,7 };
void* ret = my_memcpy(arr1, arr2, sizeof(int) * 5);
for (int i = 0; i < 5; i++)
{
printf("%d ", ((int*)ret)[i]);
}
return 0;
}
二、memmove库函数介绍
1. memmove的使用
memmove和memcpy的使用是一样的,不过它专门用于重叠内存的拷贝。
2. memmove的模拟
menmove的情况就比较多了,因为对于重叠的内存,那就得考虑第一种是与需要拷贝字节内有重叠部分,这时候就是类型指针用于放入拷贝的内容地址比另一个指针类型用于输s出内容到拷贝指针小(也就是改变后,对后面拷贝没有影响)第二种就是需要拷贝的字节内容有重叠部分,此时类型指针用于放入拷贝的内容地址比另一个指针类型用于输出内容到拷贝指针大(也就是改变后,对后面拷贝有影响),第三种就是拷贝字节不重叠(完全错开),这个是较为简单。直接上图来解释
#include<stdio.h>
#include<assert.h>
void* my_memmove(void* dest, 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);
//一步到位,用num来控制循环,还能使得从后到前改变每个字节
//这里要注意的是一开始加的是19,而不是20,所以这里是num--
}
}
return ret;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
void* ret = my_memmove(arr + 2, arr, sizeof(int) * 5);
return 0;
}
对比memmove,模拟出来的恰好的正确的。这里需要注意的就是num--,为什么要这样设计。
三、memset库函数介绍
memset虽然是内存的修改,但是对于整型修改时,可能会不符合预期,memset的使用格式为(void*)memset(void*(地址),int(修改值字符或者数字),size_t(需要修改的字节个数)),返回的是修改后的起始地址。上面也说了,memset是一个一个字节的修改对于要修改的值,所以对于数字整型4个字节的修改时,就不会符合预期。
每个字节都会修改成1,而读取整型时是通过4个字节4个字节的读取的,所以这里数字就会0x01010101 这16进制数字将会是很大的数,一般我们修改整型时是将其改为0。不过这里对于是一个字节类型,就没有影响,修改为什么值就是什么值。
四、memcmp库函数介绍
memcmp是用来对比内存中对应的数字大小(这里字符也是数字,因为字符在内存中是以ascll值存储的),用法就是(int)memcmp(void* prt1,void* prt2,size_t(需要对比的字节个数))。
#include<stdio.h>
#include<string.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 1,2,3,4,0x80000001 };
//这里以小端存储,所以这里在内存存储是0x01 00 00 80
//实际打印时还是打印出0x80 00 00 01
int r = memcmp(arr1, arr2, 17);
if (r > 0)
{
printf("arr1 > arr2\n");
}
else if (r < 0)
{
printf("arr1 < arr2\n");
}
else
{
printf("arr1 == arr2\n");
}
return 0;
}
这里是最好反映出memcmp的内部比较的形式,这里是以小端存储。对整型在内存的存储有兴趣的可以看这里
五、动态内存中malloc和free
1. malloc
malloc这个库函数是用与分配空间内存,需要头文件stdlib.h,用法为(void*)malloc (size_t(申请字节)),这里申请成功返回该空间的起始地址,如果申请失败则返回空指针,这里分配的是连续可用的空间。
#include<stdio.h>
#include<stdlib.h>
int main()
{
//开辟空间
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用空间
for (int i = 0; i < 10; i++)
{
p[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放空间
free(p);
p = NULL;
return 0;
}
2. free
这里free这个用法很简单,就是用来释放空间的函数,就好比图书馆借书,借了书肯定是要换的,这里开辟空间就类似借书这个过程,还书就是释放空间。(void) free(void* prt),内部是指向需要释放内存的这个空间,而prt指的是这个需要释放内存的这个空间的起始地址。这里必须是开辟的空间,否则会报错,如果释放的是空指针,那什么free什么都不做。
这里需要注意的是free释放完后,要养成一个好习惯,就是将这个指针变为空指针,因为后续如果要使用时,直接使用,那就是野指针的泛滥了。
六、动态内存中calloc和realloc
1. calloc
calloc这个库函数和malloc这个类似,也是开辟空间的一个库函数,格式有所不同,就是(void*) calloc((元素个数),size_t(一个元素的字节)),都是开辟空间,只不过calloc开辟一个空间后是有自己的初始化内容的,这个初始化内容全为0;要说还有一个不一样的就是语法格式吧,malloc直接就是申请字节个数,而calloc是需要知道什么类型,该类型需要多少个元素。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p1 = (int*)calloc(10, sizeof(int));
if (p1 == NULL)
{
perror("calloc");
return 1;
}
//使用空间
//释放空间
free(p1);
p1 = NULL;
return 0;
}
左图为calloc这个库函数开辟的空间,右边为malloc这个库函数开辟的空间。
2. realloc
realloc这个用于扩容或缩减内存的大小,可以看需求给他增减空间大小,用法(void*)realloc((void*prt)表示需要扩容或缩减的地址,(size_t)表示扩容或者缩减后的大小)。如果扩容成功则会返回扩容成功后的起始地址并拷贝了扩容之前地址的内容,如果扩容缩减失败则会返回NULL空指针。这里扩容有2种情况
#include<stdio.h>
#include<stdlib.h>
int main()
{
//开辟空间
int* p = malloc(sizeof(int) * 5);
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用空间
for (int i = 0; i < 5; i++)
{
p[i] = i + 1;
}
//不够扩容
int* prt = (int*)realloc(p, sizeof(int) * 10);
if (prt != NULL)
{
p = prt;//将扩容的地址赋给p(这里主要是怕第二种情况)
prt = NULL;
}
else
{
perror("realloc");
return 1;
}
//使用
//释放空间
free(p);
p = NULL;
return 0;
}
realloc也可以实现malloc功能,realloc(NULL,40)等价于开辟40个字节空间,返回开辟后的空间。 这里如果是缩减空间,缩减过多可能会导致某个溢出字节数据丢失。
七、柔性数组
1.1 柔性数组特点
柔性数组是指结构体中成员最后一项为一个数组并且还要是未知数组(没有[]大小),特点为:不参与整个结构体的大小(前提是还没给它开辟空间。就是整个结构体大小不算这个数组大小)。
#include<stdio.h>
struct Stu1
{
int age;
char name;
};
struct Stu2
{
int age;
char name;
int arr[];
};
int main()
{
printf("%zd\n", sizeof(struct Stu1));
printf("%zd\n", sizeof(struct Stu2));
return 0;
}
1.2 柔性数组的使用
柔性数组的使用一般配合动态内存函数一起使用,可以随时动态改变所需的大小,一般开辟空间大于结构体总大小,
#include<stdio.h>
#include<stdlib.h>
struct Stu
{
int age;
char name;
int num[];
};
int main()
{
//开辟空间
struct Stu* s1 = (struct Stu*)malloc(sizeof(struct Stu) + sizeof(int) * 5);
if (s1 == NULL)
{
perror("malloc");
return 1;
}
//使用
s1->age = 18;
s1->name = 'A';
for (int i = 0; i < 5; i++)
{
s1->num[i] = i + 1;
}
//空间不够
struct Stu* ps = (struct Stu*)realloc(s1, sizeof(struct Stu) + sizeof(int) * 10);
if (ps == NULL)
{
perror("realloc");
return 1;
}
//继续使用
for (int i = 5; i < 10; i++)
{
ps->num[i] = i + 1;
}
//释放
free(ps);
free(s1);
s1 = NULL;
ps = NULL;
return 0;
}
1.3 柔性数组与结构体内部成员指针区别
直接代码展示结构体内部成员为指针是如何和柔性数组一样操作的。
#include<stdio.h>
#include<stdlib.h>
struct Stu
{
int age;
char name;
int* arr;
}*s1;
int main()
{
//开辟
s1 = (struct Stu*)malloc(sizeof(struct Stu));
if (s1 == NULL)
{
perror("malloc1");
return 1;
}
//使用
s1->age = 18;
s1->name = 'A';
s1->arr = (int*)malloc(sizeof(int) * 5);//申请
if (s1->arr == NULL)
{
perror("malloc2");
return 1;
}
//使用
for (int i = 0; i < 5; i++)
{
s1->arr[i] = i + 1;
}
//扩容
int* prt = (int*)realloc(s1->arr, sizeof(int) * 10);
if (prt == NULL)
{
perror("realloc");
return 1;
}
else
{
s1->arr = prt;
prt = NULL;
}
//使用
//释放
free(s1->arr);//先释放内部再释放外部
s1->arr = NULL;
free(s1);
s1 = NULL;
return 0;
}
一般我们都是使用柔性数组,这种用指针的代码量太大了,而且需要开辟两个连续的空间,合起来就不连续,会造成很多内存碎片(表示开辟空间过多,介于两个连续空间内部剩余的空间为内存碎片)。柔性数组的优点就是1.内存释放过程中方便,不像指针需要释放两次,而且还有顺序可言。2.有利于内存访问(连续内存便于访问)。
八、动态内存中使用常见错误
1. 对NULL指针的解引⽤操作
开辟空间时,如果不判断是否为空指针,并且阻断后面进程,那就会报警告。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 5);
if (p == NULL)
{
return 1;//必要性,否则会报错
}
//使用
//释放
free(p);
p = NULL;
return 0;
}
2. 对动态开辟空间的越界访问
对于已经开辟的空间,使用了未开辟的空间,会导致越界访问。
#include<stdio.h>
#include<stdlib.h>
int main()
{
//开辟
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
for (int i = 0; i <= 10; i++)
{
*(p + i) = i + 1;//i=10时越界了
}
for (int i = 0; i <= 10; i++)
{
printf("%d ", p[i]);
}
//释放
free(p);
p = NULL;
return 0;
}
3. 对⾮动态开辟内存使⽤free释放
对于不是动态函数开辟的内存,使用free释放,会导致程序错误。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int arr[10] = { 0 };
int* p = (int*) & arr;
free(p);
p = NULL;
return 0;
}
4. 使⽤free释放⼀块动态开辟内存的⼀部分
我们知道free(*p)括号里面指向的是开辟空间,而p是开辟空间的起始地址,而释放一部分地址,也会导致程序错误,不能正常工作。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 5);
if (p == NULL)
{
perror("malloc");
return 1;
}
p++;
free(p);
p = NULL;
return 0;
}
5. 对同⼀块动态内存多次释放
对一个已经开辟的动态内存多次释放。
#include<stdio.h>
#include<stdlib.h>
int main()
{
//开辟
int* p = (int*)malloc(sizeof(int) * 5);
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
//释放
free(p);
free(p);
p = NULL;
return 0;
}
6. 动态开辟内存忘记释放(内存泄漏)
在函数中开辟空间,如果不释放空间并且不返回该空间起始地址,会导致这一块空间浪费,既不能使用,也不能再次开辟这一块空间。
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(sizeof(int) * 5);
if (p == NULL)
{
perror("malloc");
p = NULL;
}
//忘记释放
}
int main()
{
test();
return 0;
}
九、关于动态内存易错例题
1. 题目一
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
//1.没有对NULL解引判断用
//2.内存泄漏(没有释放空间)
}
void Test(void)
{
char* str = NULL;
GetMemory(str);//这里是传值调用
strcpy(str, "hello world");
//str为空指针
printf(str);
}
int main()
{
Test();
return 0;
}
根据错误信息修改:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
if (*p == NULL)//判断
{
perror("malloc");
return;
}
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);//传址
strcpy(str, "hello world");
printf(str);
free(str);//释放
str = NULL;
}
int main()
{
Test();
return 0;
}
2. 题目二
#include<stdio.h>
#include<stdlib.h>
char* GetMemory(void)
{
char p[] = "hello world";
//函数中的局部变量在栈区离开后,会释放内存
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
//str已经改变
//但是使用过程中会变为野指针
printf(str);
}
int main()
{
Test();
return 0;
}
3. 题目三
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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;
}
4. 题目四
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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;
}
十、总结c/c++中程序内存区域划分图
