当前位置:   article > 正文

C进阶-动态内存管理+柔性数组

C进阶-动态内存管理+柔性数组

目录

1.为什么会存在动态内存分配

2.动态内存函数的介绍

2.1 malloc和free

2.2 calloc

2.3 realloc

3.常见的动态内存错误

3.1 对NULL指针的解引用操作

3.2 对动态开辟空间的越界访问

3.3 对非动态开辟内存使用free释放

3.4 使用free释放一块动态开辟内存的一部分

3.5 对同一块动态内存多次释放

3.6 动态开辟内存忘记释放(内存泄漏)

4.几个经典的笔试题

4.1 题目1:

4.2 题目2:

4.3 题目3:

4.4 题目4:

5.C/C++的内存开辟

6.柔性数组

6.1柔性数组的特点

6.2柔性数组的使用

 6.3柔性数组的优势


1.为什么会存在动态内存分配

根据我们学过的知识,要在内存中开辟一个空间,有两种方式:

  1. int c = 10;//创建一个变量
  2. int arr[10] = { 0 };//创建一个数组

但是上述开辟空间的方式有两个局限性:

1.空间开辟的大小是固定的

2.数组在声明的时候,必须指定数组的长度,他所需要的内存在编译时分配

但是有时候,我们只有在运行程序的时候,才能知道自己需要多大的空间,这时候空间已经开辟好了,不能再改变了,有可能大了,也有可能小了,所以上述开辟方式可能会在我们使用的时候带来极大的不便,这就是我们需要动态内存开辟的原因,动态内存管理会根据我们的需要,对开辟的空间进行放大和缩小。

2.动态内存函数的介绍

动态内存管理函数有4个:malloc、calloc、realloc、free

2.1 malloc和free

void*  malloc( size_t size );

这个函数向内存申请一块连续的空间,并返回指向这块空间的指针。

  1. int main()
  2. {
  3. int arr[10] = { 0 };
  4. int* p = (int*)malloc(40);
  5. return 0;
  6. }

上述代码中,我们用数组申请了10个整型的空间,也可以用malloc向内存申请10个整型的空间(40个字节) 。

注意我们将这块空间的地址赋给指针p时,指针p是int*型,而malloc函数的返回类型是void*,所以要强制类型转化为int*。

这样我们就用malloc申请了空间,下面我们就可以直接使用了吗?

可能不行,因为malloc申请空间也是会失败的:

如果开辟成功,则返回一个指向开辟好的空间的指针。

如果开辟失败,则返回一个NULL指针

因此malloc的返回值一定要做检查:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main()
  4. {
  5. int* p = (int*)malloc(40);
  6. if (p == NULL)
  7. {
  8. perror("malloc");//打印错误信息
  9. return 1;
  10. }
  11. //开辟成功
  12. int i = 0;
  13. for (i = 0; i < 10; i++)
  14. {
  15. printf("%d ", *(p + i));
  16. }
  17. return 0;
  18. }

用if语句判断是否开辟成功,如果开辟失败,则用perror函数打印错误信息,如果开辟成功,则打印出来。(注意使用perror时,要包含头文件<stdlib.h>

下面我们再来讲一下malloc函数在内存中开辟的空间的具体体现:

之前我们学过,内存中分为栈区、堆区、静态区。栈区存放的是局部变量和形式参数等,静态区存放的是全局变量和静态变量等,那堆区存放的是什么呢?

就是动态内存开辟。

malloc函数申请的空间在堆区,指针变量p开辟的空间在栈区,存放所申请空间的地址。 

malloc申请到空间后,直接返回这块空间的地址,不初始化空间的内容,所以上述代码打印的结果是:

而当程序退出时,malloc申请的内存空间不会主动还给操作系统,这时候就需要用free函数了:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main()
  4. {
  5. int* p = (int*)malloc(40);
  6. if (p == NULL)
  7. {
  8. perror("malloc");//打印错误信息
  9. return 1;
  10. }
  11. //开辟成功
  12. int i = 0;
  13. for (i = 0; i < 10; i++)
  14. {
  15. printf("%d\n", *(p + i));
  16. }
  17. //释放空间
  18. free(p);
  19. p = NULL;
  20. return 0;
  21. }

当free释放掉malloc申请的内存空间后,此时栈区的指针变量p还在啊,它里面还从存放着已经释放掉空间的地址,如果我们不对它进行处理,那p就成了野指针,所以在free(p)后面还令p=NULL。

我们在使用free函数时也要注意,不是动态内存开辟的空间不能用free函数释放,如下面的写法就是错误的:

  1. int p = 0;
  2. int* ptr = &p;
  3. free(ptr);//error

2.2 calloc

void* calloc( size_t num, sizr_t size )

calloc函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。

与函数malloc函数的区别是calloc函数会在返回前将申请的空间的每一个字节初始化为0。

看下面一段代码:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main()
  4. {
  5. int* p = (int*)calloc(40, sizeof(int));
  6. if (p == NULL)
  7. {
  8. perror("calloc\n");
  9. return 1;
  10. }
  11. //打印数据
  12. int i = 0;
  13. for (i = 0; i < 10; i++)
  14. {
  15. printf("%d ", p[i]);
  16. }
  17. free(p);
  18. p = NULL;
  19. return 0;
  20. }

运行结果:

我们可以看到calloc函数开辟空间后确实将空间中每个字节初始化为全0

 当然,我们也可以来看一下开辟失败的结果:

以上就是calloc函数,它和malloc函数有区别,但是功能差不多,下面我们来讲最最重要的realloc函数:

2.3 realloc

realloc函数让动态内存管理更加灵活。

有时候我们发现申请的空间太小了,有时候我们又会觉得申请的空间太小了,那为了合理利用空间,我们一定会对内存的大小进行调整,那realloc函数就可以做到对动态开辟内存大小的调整。

vioe* realloc( void* ptr,size_t size )

ptr是要调整的内存地址,size是调整之后的大小 

realloc函数的返回值是调整之后的内存起始地址,这个地址有可能和之前开辟空间的地址一样,也有可能是一个新的地址,为什么这么说呢?

这就要提到realloc函数调整空间的两种情况了。

假设我们已经用malloc函数开辟了40个字节的空间,但是我们用的时候觉得不够了,需要再增加40个字节的空间,此时要用realloc将空间大小调整为80个字节,但是realloc在调整时会出现如下两种情况:

下面来看一段代码:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main()
  4. {
  5. int* p = (int*)malloc(40);
  6. if (p == NULL)
  7. {
  8. perror("malloc");
  9. return 1;
  10. }
  11. //初始化1~10
  12. int i = 0;
  13. for (i = 0; i < 10; i++)
  14. {
  15. p[i] = i + 1;
  16. }
  17. //增加空间
  18. p = realloc(p, 80);
  19. free(p);
  20. p = NULL;
  21. return 0;
  22. }

 上述代码中,用p直接来接收realloc的返回地址行不行?

当然不行,要知道realloc函数开辟空间也会失败的,要是开辟成功了,我们用p接收可以,但是要是开辟失败了,这时realloc函数就会返回一个空指针NULL,那此时我们malloc函数开辟的空间的起始地址也是p啊,里面还存放着10个值呢?要是用p接收了空指针,我们这些数据该怎么办?

所以最好为realloc函数返回的新空间的起始地址重新创建一个指针变量ptr,经过判断后再将ptr赋给p:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main()
  4. {
  5. int* p = (int*)malloc(40);
  6. if (p == NULL)
  7. {
  8. perror("malloc");
  9. return 1;
  10. }
  11. //初始化1~10
  12. int i = 0;
  13. for (i = 0; i < 10; i++)
  14. {
  15. p[i] = i + 1;
  16. }
  17. //增加空间
  18. int*ptr = (int*)realloc(p, 80);
  19. if (ptr != NULL)
  20. {
  21. p = ptr;
  22. ptr = NULL;
  23. }
  24. else
  25. {
  26. printf("realloc");
  27. return 1;
  28. }
  29. //打印数据
  30. for (i = 0; i < 20; i++)
  31. {
  32. printf("%d\n", p[i]);
  33. }
  34. //释放空间
  35. free(p);
  36. p = NULL;
  37. return 0;
  38. }

 打印结果(可以看到前10个数据还在,后面又开辟了10个int型大小的空间):

以上就是使用realloc函数增加空间,要想减少空间的话,将传给size的值变小点就行了。

还有一点,要是传给realloc函数参数ptr的是空指针NULL,此时realloc函数和malloc函数的功能一样。

以上就是动态内存管理的4个函数的介绍。

3.常见的动态内存错误

3.1 对NULL指针的解引用操作

  1. int main()
  2. {
  3. int* p = (int*)malloc(INT_MAX);
  4. *p = 20;//如果p是NULL,就会出现问题
  5. free(p);
  6. return 0;
  7. }

这个我们上文也讲过,要对p进行判断,是不是空指针,如果不是再使用。

3.2 对动态开辟空间的越界访问

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main()
  4. {
  5. int* p = (int*)malloc(40);
  6. if (p == NULL)
  7. {
  8. perror("malloc");
  9. return 1;
  10. }
  11. int i = 0;
  12. for (i = 0; i < 20; i++)
  13. {
  14. p[i] = i + 1;//开辟了10个整型,访问20个整型,越界访问了
  15. }
  16. free(p);
  17. p = NULL;
  18. return 0;
  19. }

 3.3 对非动态开辟内存使用free释放

上文中讲过:

  1. int p = 0;
  2. int* ptr = &p;
  3. free(ptr);//error

3.4 使用free释放一块动态开辟内存的一部分

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main()
  4. {
  5. int* p = (int*)malloc(40);
  6. if (p == NULL)
  7. {
  8. perror("malloc");
  9. return 1;
  10. }
  11. int i = 0;
  12. for (i = 0; i < 5; i++)
  13. {
  14. *p = i ;
  15. p++;
  16. }
  17. free(p);
  18. p = NULL;
  19. return 0;
  20. }

 上述代码中,我们使用了开辟的空间中的前5个元素,但是注意在使用后进行p++,那当我们运行完,p指向的就是第5个元素所在空间的地址,此时再用free释放,释放的是第5个元素后面的空间,这样程序会崩溃,也就是说,不能使用free释放一块动态开辟内存的一部分

3.5 对同一块动态内存多次释放

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main()
  4. {
  5. int* p = (int*)malloc(40);
  6. if (p == NULL)
  7. {
  8. perror("malloc");
  9. return 1;
  10. }
  11. free(p);
  12. //
  13. free(p);
  14. p = NULL;
  15. return 0;
  16. }

我们在写代码的时候很可能出现,前面已经对这块内存释放过了,后面忘记了,又释放了一次,这时就出现错误了,所以最好养成一个习惯,就是在每次释放之后,将p置为NULL,这样即使重复释放,后面的free也没有任何作用。

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main()
  4. {
  5. int* p = (int*)malloc(40);
  6. if (p == NULL)
  7. {
  8. perror("malloc");
  9. return 1;
  10. }
  11. free(p);
  12. p = NULL;
  13. free(p);
  14. p = NULL;
  15. return 0;
  16. }

 3.6 动态开辟内存忘记释放(内存泄漏)

看下面一段代码:

  1. void test()
  2. {
  3. int *p = (int *)malloc(100);
  4. if(NULL != p)
  5. {
  6. *p = 20;
  7. }
  8. }
  9. int main()
  10. {
  11. test();
  12. while(1);
  13. }

上述代码中,我们在test函数中用malloc申请了100个字节空间,当test函数结束时,指针变量p就自动销毁了,但是malloc申请的那100个字节的空间还在(没有用free释放),只要程序不结束,它就永远不会销毁,而我们在主函数中写了一个while(1)死循环,所以动态开辟的空间泄露了。

内存泄漏造成的问题很严重,它可能会使电脑崩溃,像我们生活中使用的各种APP,之所以我们不论何时登录上去都能使用,是因为它每时每刻都在运行,而要是内存泄漏的话,它每运行一次内存就泄漏一点,直到有一天内存被泄露完了,你的电脑也就崩溃了,这时如果重启一下电脑会发现电脑又好了,但是一旦你打开那个APP,多次使用,总有一天你的电脑又会崩溃。

总结一下,动态内存开辟的空间不会因为出了作用域就销毁,只有两种方式销毁(还给操作系统):1.free  2.程序结束(退出)。

4.几个经典的笔试题

4.1 题目1:

  1. void GetMemory(char *p)
  2. {
  3. p = (char *)malloc(100);
  4. }
  5. void Test(void)
  6. {
  7. char *str = NULL;
  8. GetMemory(str);
  9. strcpy(str, "hello world");
  10. printf(str);
  11. }

请问运行test函数会出现什么样的结果?能不能打印出“hello world”?

答案是不能,我们来分析一下原因:

要想成功打印出“hello world”,传参的时候就应该传&str。

上述代码还有一处错误,就是没有释放动态内存开辟的空间,所以改正后的代码应该是如下写法:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. void GetMemory(char** p)
  4. {
  5. *p = (char*)malloc(100);
  6. }
  7. void Test(void)
  8. {
  9. char* str = NULL;
  10. GetMemory(&str);
  11. strcpy(str, "hello world");
  12. printf(str);
  13. //释放内存
  14. free(str);
  15. str = NULL;
  16. }
  17. int main()
  18. {
  19. Test();
  20. return 0;
  21. }

有人说,上面printf(str)的写法不是错误的吗?

其实这种写法没有错,因为我们在打印字符串时用的是printf("hello world"),这里我们传给printf函数的其实只是首字符h的地址,而如果我们把字符串赋给指针char*p="hello world";这里p中存的也是首字符h的地址,那要打印的时候就可以用printf(p)。

同理,上述写法也能打印出"hello world"

 4.2 题目2:

  1. char *GetMemory(void)
  2. {
  3. char p[] = "hello world";
  4. return p;
  5. }
  6. void Test(void)
  7. {
  8. char *str = NULL;
  9. str = GetMemory();
  10. printf(str);
  11. }

运行这段代码会发现,打印出来一串随机值,这是为什么呢?

上述代码中p只是一个局部变量,它在出了函数GetMeory后就会销毁,而出函数GetMeory之前return p返回了p所指向空间的地址,当str根据p的地址找过去时,p所指向的空间已经销毁,str就成了野指针,所以就会非法访问了。

如果要改正上述代码,我们只需要加上static延长p的生命周期就行:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. char* GetMemory(void)
  4. {
  5. static char p[] = "hello world";
  6. return p;
  7. }
  8. void Test(void)
  9. {
  10. char* str = NULL;
  11. str = GetMemory();
  12. printf(str);
  13. }
  14. int main()
  15. {
  16. Test();
  17. }

下面我们再来举一个相似的例子:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int* test()
  4. {
  5. int a = 10;
  6. return &a;
  7. }
  8. int main()
  9. {
  10. int* p = test();
  11. printf("%d\n", *p);
  12. }

 这段代码实际上和上文的代码是一样的道理,但是我们运行一下会发现,竟然打印出了10,这是为什么?

其实很容易解释,当进入test函数后,为a变量开辟了一块空间存放10,返回了a的地址,我们在主函数中用p接收到了这个地址,然后根据*p打印,此时虽然a已经销毁,我们依旧侥幸找到了存放10的空间,但是如果我们在printf函数之前任意写一段代码,那要为这段代码开辟空间,就会立即覆盖掉a的空间,这样打印出的值就不是10了。

以上题目统称为返回栈空间地址的问题

在栈上开辟空间的变量,进入作用域创建,出了作用域,它就销毁了,如果你在出作用域之前将该变量的地址返回了,并且在其他地方用指针接受了,那这个指针就变成了一个野指针。

4.3 题目3:

  1. void GetMemory(char **p, int num)
  2. {
  3. *p = (char *)malloc(num);
  4. }
  5. void Test(void)
  6. {
  7. char *str = NULL;
  8. GetMemory(&str, 100);
  9. strcpy(str, "hello");
  10. printf(str);
  11. }

 这个代码看起来完全正常,但是有一点它忘记了,就是对动态开辟空间的释放,所以只要加上free就行:

  1. void GetMemory(char **p, int num)
  2. {
  3. *p = (char *)malloc(num);
  4. }
  5. void Test(void)
  6. {
  7. char *str = NULL;
  8. GetMemory(&str, 100);
  9. strcpy(str, "hello");
  10. printf(str);
  11. free(p);
  12. p=NULL;
  13. }

4.4 题目4:

  1. void Test(void)
  2. {
  3. char *str = (char *) malloc(100);
  4. strcpy(str, "hello");
  5. free(str);
  6. if(str != NULL)
  7. {
  8. strcpy(str, "world");
  9. printf(str);
  10. }
  11. }

 这段代码也是非法访问内存了,在给str开辟了100个字节的空间后,用strcpy函数将"hello"拷贝进去了,接着就释放了这块空间,但是在if语句中,又对str用strcpy函数想将"world"拷贝进去,此时str所指向空间已经被释放,所以非法访问了。

改正(释放空间后,将str置为NULL):

  1. void Test(void)
  2. {
  3. char *str = (char *) malloc(100);
  4. strcpy(str, "hello");
  5. free(str);
  6. str = NULL;
  7. if(str != NULL)
  8. {
  9. strcpy(str, "world");
  10. printf(str);
  11. }
  12. }

5.C/C++的内存开辟

下面通过一张图来了解一下C/C++中程序内存区域的划分:

有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。 

上文中我们也讲过,全局变量和静态变量是存放在静态区中的,局部变量和形式参数是存放在栈区中的,而堆区中存放的是malloc、calloc、realloc开辟的空间,下面我们来看一个例子:

通过上图打印出来的地址,会发现存放在栈区的a、b的地址接近,存放在静态区的c、d的地址接近。 

以上就是动态内存分配的全部内容,下面我们来讲一个特殊的数组--柔性数组

6.柔性数组

C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员

例如:

  1. struct S
  2. {
  3. int n;
  4. int arr[];//柔性数组成员
  5. }
  6. int main()
  7. {
  8. retrun 0;
  9. }

有些编译器中编译不过,可以将arr[]写成arr[0]。

  1. struct S
  2. {
  3. int n;
  4. int arr[0];//柔性数组成员
  5. }
  6. int main()
  7. {
  8. retrun 0;
  9. }

6.1柔性数组的特点

1. 结构中的柔性数组成员前面必须至少一个其他成员。

这个前面的代码就可以看出来,struct S中的柔性数组前面有一个成员n。

2.sizeof返回的结构大小不包含柔性数组内存

下面我们可以用sizeof计算一下结构的大小:

可以看到计算的结果是4,一个int型的变量n的大小就是4,由此可见,sizeof在计算结构大小的时候不包含柔性数组的大小。

3. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

下面我们来为struct S开辟空间:

  1. #include<stdio.h>
  2. struct S
  3. {
  4. int n;
  5. int arr[];
  6. };
  7. int main()
  8. {
  9. struct S*ps=(struct S*)malloc(sizeof(struct S) + 40);
  10. return 0;
  11. }

上述代码是我们在结构体成员变量n的基础上一次性开辟44个字节的空间,40是柔性数组预期的大小。

6.2柔性数组的使用

我们也可以对上述结构体类型的变量初始化:

  1. #include<stdio.h>
  2. struct S
  3. {
  4. int n;
  5. int arr[];
  6. };
  7. int main()
  8. {
  9. struct S*ps=(struct S*)malloc(sizeof(struct S) + 40);
  10. if (ps == NULL)
  11. {
  12. perror("malloc");
  13. return 1;
  14. }
  15. ps->n = 100;
  16. int i = 0;
  17. for (i = 0; i < 10; i++)
  18. {
  19. ps->arr[i] = i + 1;
  20. }
  21. //释放
  22. free(ps);
  23. ps = NULL;
  24. return 0;
  25. }

上述代码中开辟的内存空间如下图所示:

当我们觉得空间不够用了,也可以用realloc增容:

  1. #include<stdio.h>
  2. struct S
  3. {
  4. int n;
  5. int arr[];
  6. };
  7. int main()
  8. {
  9. struct S*ps=(struct S*)malloc(sizeof(struct S) + 40);
  10. if (ps == NULL)
  11. {
  12. perror("malloc");
  13. return 1;
  14. }
  15. ps->n = 100;
  16. int i = 0;
  17. for (i = 0; i < 10; i++)
  18. {
  19. ps->arr[i] = i + 1;
  20. }
  21. struct S* ptr = (struct S*)realloc(ps,sizeof(struct S) + 60);
  22. if (ptr != NULL)
  23. {
  24. ps = ptr;
  25. }
  26. else
  27. {
  28. perror("realloc");
  29. return 1;
  30. }
  31. ps->n = 15;
  32. for (i = 0; i < 15; i++)
  33. {
  34. printf("%d\n", ps->arr[i]);
  35. }
  36. //释放
  37. free(ps);
  38. ps = NULL;
  39. return 0;
  40. }

 打印结果:


 

 6.3柔性数组的优势

其实上述柔性柔性数组的使用,我们也可以用一下代码来模拟它:

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. struct S
  5. {
  6. int n;
  7. int* arr;
  8. };
  9. int main()
  10. {
  11. struct S* ps = (struct S*)malloc(sizeof(struct S));
  12. if (ps == NULL)
  13. {
  14. perror("malloc");
  15. return 1;
  16. }
  17. ps->n = 100;
  18. ps->arr = (int*)malloc(40);
  19. if (ps->arr == NULL)
  20. {
  21. perror("malloc->arr");
  22. return 1;
  23. }
  24. int i = 0;
  25. for (i = 0; i < 10; i++)
  26. {
  27. ps->arr[i] = i + 1;
  28. }
  29. //调整
  30. int* ptr = (int*)realloc(ps->arr, 60);
  31. if (ptr != NULL)
  32. {
  33. ps->arr = ptr;
  34. }
  35. else
  36. {
  37. perror("realloc");
  38. return 1;
  39. }
  40. //打印
  41. for (i = 0; i < 15; i++)
  42. {
  43. printf("%d\n", ps->arr[i]);
  44. }
  45. //释放
  46. free(ps->arr);
  47. ps->arr = NULL;
  48. free(ps);
  49. ps = NULL;
  50. return 0;
  51. }

注意使用了两次malloc,所以要free两次,先free内层的,后free外层的。

 运行结果:

上述代码开辟空间的方式与柔性数组不同,经过两次开辟得到:

 

虽然开辟空间方式不同,但是第二种方式和柔性数组一样,n和arr都在堆区,也能通过realloc实现增加空间,也可以进行赋值和打印,由此可见我们第二种实现方法也能像柔性数组一样,那为什么还要使用柔性数组呢?

柔性数组也是有优势的,

首先,第二种方式虽然实现了柔性数组的功能,但是开辟空间是用了两次malloc,而用了malloc就要free,一旦你忘记free就有可能出现错误。

其次,我们说开辟的内存和内存之间是有缝隙的,malloc用的越多,缝隙(内存碎片)越多,对内存的利用率就越低,而柔性数组只用了一次malloc,所以柔性数组对内存的利用率比较高。

还有,柔性数组开辟的内存是连续的,这就意味着柔性数组的访问速度更快。

总结一下柔性数组的优势:

1.方便内存释放,对内存的利用率高

2.有利于提高访问速度

那么到这就是我们今天的全部内容了,未完待续。。。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/670478
推荐阅读
相关标签
  

闽ICP备14008679号