C语言 --- 动态内存管理(上)+优化版通讯录+笔试题
文章目录
- 前言
- 一、为什么存在动态内存分配
- 二、动态内存函数的介绍
- 2.1.malloc函数+free函数
- 2.2.calloc函数+free函数
- 2.3.realloc函数
- 三、常见的动态内存错误
- 3.1.对NULL指针的解引用操作
- 3.2.对动态开辟空间的越界访问
- 3.3.对非动态开辟内存使用free释放
- 3.4.使用free释放一块动态开辟内存的一部分
- 3.5.对同一块动态内存多次释放
- 3.6.动态开辟内存忘记释放(内存泄漏)
- 四、优化通讯录
- 4.1.优化前后通讯录的创建
- 4.2.优化前后通讯录的初始化
- 4.3.优化后通讯录的增加人数
- 4.4.优化后通讯录的退出
- 4.5.完整的通讯录
- 五、一些笔试题
- 总结
前言
当你的能力还驾驭不了你的目标时那你就应该沉下心来历练
上章节我们利用之前所学的知识制作了静态内存通讯录。正因为是静态所以存在一些问题,今天我们就来学习动态内存管理。
提示:以下是本篇文章正文内容,下面案例可供参考
一、为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;在栈空间上开辟四个字节
char arr[10] = {0};在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1.空间开辟大小是固定的。
⒉.数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态存开辟了。
二、动态内存函数的介绍
2.1.malloc函数+free函数
C语言提供了一个动态内存开辟的函数:
void*malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。*
- 如果参数size为0,malloc的行为是标准是未定义的,取决于编译器。
C语言提供了另外一个函数free,专门 是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
free函数用来释放动态开辟的内存。
- 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数ptr是NULL指针,则函数什么事都不做。
malloc和free都声明在stdlib.h头文件中。
各位不要忘了我们之前学数组和指针的时候学过(p+i)==p[i],所以也可以写成p[i]
#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
int main()
{//申请//我们放整形时malloc是void*所以强制类型转换赋值给int* pint* p=(int*)malloc(20);//malloc这个函数一旦申请空间失败就会返回一个空指针if (p == NULL){printf("%s\n", strerror(errno));return 1;}//开辟成功 --- 使用int i = 0;for (i = 0; i < 5; i++){*(p + i) = i + 1;//为了防止p的地址被改变导致后面释放出错所以我们不用p++而是p+i}for (i = 0; i < 5; i++){printf("%d", *(p + i));}//释放free(p);//free函数并不会让p置为空指针//free将所开辟的空间释放还给系统p = NULL;//避免了野指针return 0;
}
我们可以在调试内存中看到我们把所想要放入的值放进去了
注意:
malloc申请空间时,里面放的都是随机值
2.2.calloc函数+free函数
C语言还提供了一个函数叫ca1loc,cal1oc函数也用来动态内存分配。原型如下:
void* calloc (size_t num,size_t size);
- 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
- ·与函数ma1loc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
#include<stdio.h>
#include<errno.h>
#include<stdlib.h>
int main()
{//开辟int* p = (int*)calloc(10, sizeof(int));if (p == NULL){printf("calloc()---%s\n", strerror(errno));}//使用int i = 0;for (i = 0; i < 10; i++){printf("%d ", p[i]);}//释放free(p);p = NULL;return 0;
}
根据运行结果我们可以看出calloc是由初始化的而malloc是随机值
注意:
calloc和malloc的对比:
1.参数不一样
⒉都是在堆区上申请内存空间,但是malloc不初始化,calloc会初始化为0如果要初始化,就使用calloc不需要初始化,就可以使用malloc
2.3.realloc函数
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那rea1loc函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* rea1loc (void* ptr,size_t size);
- ptr是要调整的内存地址
- size调整之后新大小(这里是字节哦~)
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
#include<stdio.h>
#include<errno.h>
#include<stdlib.h>
int main()
{//开辟int* p = (int*)malloc(20);if (p == NULL){printf("%s\n", strerror(errno));return 1;}//使用int i = 0;for (i = 0; i < 5; i++){p[i] = i + 1;}int* ptr = (int*)realloc(p, 40);//这里不能用p接受哦因为realloc返回空指针时,p就被赋值为空指针原来的20个字节的空间都找不到了if (ptr != NULL){p = ptr;}else{printf("realloc:%s\n", strerror(errno));return 1;}for (i = 5; i < 10; i++){p[i] = i+1;}for (i = 0; i < 10; i++){printf("%d ", p[i]);}//释放free(p);p = NULL;return 0;
}
三、常见的动态内存错误
3.1.对NULL指针的解引用操作
可能会出现对NULL指针的解引用操作
所以malloc函数的返回值要判断
#include<stdio.h>
#include<stdlib.h>
int main()
{int* p = (int*)malloc(20);int i = 0;for (i = 0; i < 5; i++){p[i] = i;}free(p);p = NULL;return 0;
}
3.2.对动态开辟空间的越界访问
#include<stdio.h>
#include<stdlib.h>
int main()
{int* p = (int*)malloc(20);if (p == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;//malloc开辟了20个字节的空间但是这里要访问的却是40个字节的空间发生了越界访问for (i = 0; i < 10; i++){p[i] = i;}free(p);p = NULL;return 0;
}
3.3.对非动态开辟内存使用free释放
#include<stdio.h>
#include<stdlib.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = arr;free(p);p = NULL;return 0;
}
3.4.使用free释放一块动态开辟内存的一部分
#include<stdio.h>
#include<stdlib.h>
int main()
{int* p = (int*)malloc(20);if (p == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;for (i = 0; i < 10; i++){*p = i + 1;p++;}free(p);p = NULL;return 0;
}
3.5.对同一块动态内存多次释放
这种问题可能会出现在一些事情上,假设A程序猿开辟一块空间后释放,但是B程序员并不知道,可能会导致二次释放。所以我们可以将p==NULL置为空指针这样就不会报错啦
#include<stdio.h>
#include<stdlib.h>
int main()
{int* p = (int*)malloc(20);if (p == NULL){printf("%s\n", strerror(errno));return 1;}//释放free(p);p = NULL;//我们释放掉把它置为空指针,p为空指针,这样就不会报错//释放free(p);p = NULL;return 0;
}
3.6.动态开辟内存忘记释放(内存泄漏)
malloc calloc realloc所申请的空间,如果不想使用,需要free释放如果不使用free释放;程序结束之后,也会由操作系统回收;
如果不使用free释放,程序也不结束就会发生内存泄露
#include<stdio.h>
void test()
{int* p = (int*)malloc(20);//使用
}
int main()
{test();//这个函数一旦返回p就被销毁了,并没有把地址返回来//出了test函数来到主函数内部,主函数并不知道这个内存在哪里所以并没被释放return 0;
}
四、优化通讯录
4.1.优化前后通讯录的创建
首先创建一个通讯录我们想起之前写过的静态通讯录
typedef struct PeoInfo
{char name[MAX_NAME];int age;char sex[MAX_SEX];char tele[MAX_TELE];char addr[MAX_ADDR];
}PeoInfo;
typedef struct Contact
{PeoInfo data[MAX];int sz;
}Contact;
那我们动态版本的怎么写呢?我们让它满足几点要求:
1.默认能够存放3个人的信息
2.不够的话,每次增加2个人信息
这样的话我们的动态版本不够的话每次需要增加两个人的信息所以就不能是数组形式了不然会出现问题malloc开辟的空间的起始地址交给一个指针维护;因为i要扩容我们要定义一个变量(capacity)来记录容量,还有定义一个变量(sz)来记录通讯录中的有效信息
typedef struct PeoInfo
{char name[MAX_NAME];int age;char sex[MAX_SEX];char tele[MAX_TELE];char addr[MAX_ADDR];
}PeoInfo;
//静态版本
typedef struct Contact
{PeoInfo *data;//不够的话每次需要增加两个人的信息所以这里不能用数组会发生问题//malloc开辟的空间的起始地址交给一个指针维护//data指向了存向数据的空间int sz;//记录通讯录中的有效信息int capacity;//记录通讯录当前的容量
}Contact;
4.2.优化前后通讯录的初始化
接下来就是初始化啦~对于数组我们初始化使用memset函数把里面的每个元素都设置为0
void InitContact(Contact* pc)
{pc->sz=0;//pc->date;//date是一个数组是一块连续的空间,数组名是地址不可以改成0//所以应该把pc所指向通讯录的date数组里的值改成0memset(pc->data, 0, sizeof(pc->data));
}
但对于动态内存的初始化,我们需要提前在之前静态的版本上再定义一个最开始默认值放在contact.h,这样方便我们日后更改默认值和扩容量
#define DEFAULT_SZ 3//初始默认值为3
#define INC_SZ 2//每次扩容2个
void InitContact(Contact* pc)
{pc->data = (PeoInfo*)malloc(DEFAULT_SZ * sizeof(PeoInfo));//最开始默认大小,但希望我们日后可以随时改变所以我们定义一个DEFAULT_SZ和INC_SZ(扩容)if (pc == NULL){printf("通讯录初始化失败:%s\n", strerror(errno));return;}pc->sz = 0;pc->capacity = DEFAULT_SZ;//默认初始值
}
4.3.优化后通讯录的增加人数
不够就增容,所以我们发现主要和增加有关系,所以我们需要判断一下是否通讯录的人数超过三个是否需要扩容,所以我们分装一个函数来完成这个行为(用realloc来完成)
注意:
这里需要注意不然容易出现bug我们扩容后,一定要pc->data = ptr;不然实际上的data并没有扩容我们超过三个人的信息就会发生越界访问,INC_SZ就是我们最开始在contact.h中定义的扩容值,所以扩容后这个记录capacity当前容量的变量会+=2;我们还要注意一点扩容失败返回0,扩容成功,或者不需要扩容,返回1
int CheckCapacity(Contact* pc)
{if (pc->sz == pc->capacity){PeoInfo*ptr=realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo));if (ptr == NULL){printf("CheckCapacity:%s\n", strerror(errno));return 0;}else{pc->data = ptr;//返回值为调整之后的内存起始位置pc->capacity += INC_SZ;printf("增容成功,当前容量:%d\n", pc->capacity);return 1;}}
}
void AddContact(Contact* pc)
{if(CheckCapacity(pc)==0){printf("空间不够,扩容失败\n");return 0;}else{printf("请输入名字>:");scanf("%s", pc->data[pc->sz].name);printf("请输入年龄>:");scanf("%d", &(pc->data[pc->sz].age));printf("请输入性别>:");scanf("%s", pc->data[pc->sz].sex);printf("请输入电话>:");scanf("%s", pc->data[pc->sz].tele);printf("请输入地址>:");scanf("%s", pc->data[pc->sz].addr);pc->sz++;printf("输入完成\n");}
}
4.4.优化后通讯录的退出
前面的修改查找删除我们不需要做更改所以我们来到最后一项对于我们malloc开辟的内存空间最后我们要释放,所以我们要在静态版本的基础上进行修改。我们分装一个DestroyContact函数,然后在contact.h中声明再在contact.c中实现
释放完成后,我们将data置为NULL,capacity置为0,sz置为0
void DestroyContact(Contact* pc)
{free(pc->data);pc->data = NULL;pc->capacity = 0;pc->sz = 0;printf("释放内存\n");
}
4.5.完整的通讯录
//contact.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define MAX 100
#define MAX_NAME 20
#define MAX_SEX 5
#define MAX_TELE 12
#define MAX_ADDR 30
//表示一个人信息
typedef struct PeoInfo
{char name[MAX_NAME];int age;char sex[MAX_SEX];char tele[MAX_TELE];char addr[MAX_ADDR];
}PeoInfo;
typedef struct Contact
{PeoInfo data[MAX];int sz;
}Contact;
//初始化通讯录
void InitContact(Contact* pc);
//增加指定联系人
void AddContact(Contact* pc);
//显示联系人信息
void ShowContact(Contact* pc);
//删除联系人信息
//void DelContact(pContact pc);
void DelContact(Contact* pc);
//查找指定联系人
void SearchContact(Contact* pc);
//修改指定联系人信息
void ModifyContact(Contact* pc);
//排序联系人
void SortContact(Contact* pc);
//contact.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"
#include<string.h>
void InitContact(Contact* pc)
{pc->sz=0;//pc->date;//date是一个数组是一块连续的空间,数组名是地址不可以改成0//所以应该把pc所指向通讯录的date数组里的值改成0memset(pc->data, 0, sizeof(pc->data));
}
void AddContact(Contact* pc)
{if (pc->sz == MAX){printf("通讯录已满,无法增加\n");return;//在函数里面遇见return就返回了,因为是void所以不需要返回值}printf("请输入名字>:");//结构体对象用 . //结构体指针用 ->scanf("%s", pc->data[pc->sz].name);//数组名本来就是一个地址所以不需要进行&操作printf("请输入年龄>:");scanf("%d", &(pc->data[pc->sz].age));//年龄是一个变量所以需要&操作printf("请输入性别>:");scanf("%s", pc->data[pc->sz].sex );printf("请输入电话>:");scanf("%s", pc->data[pc->sz].tele );printf("请输入地址>:");scanf("%s", pc->data[pc->sz].addr );pc->sz++;printf("输入完成\n");
}
void ShowContact(const Contact* pc)
{int i = 0;//printf("%10s %4s %5s %12s %30s\n", "姓名" "年龄" "性别" "电话" "地址");//打印标题printf("%-10s %-4s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");//打印数据for (i = 0; i < pc->sz; i++){printf("%-10s %-4d %-5s %-12s %-30s\n", pc->data [i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);}
}
static int FindByName(Contact*pc,char name[])//加上static这个函数只能在所在.c文件中使用
{int i = 0;for (i = 0; i < pc->sz; i++){if (strcmp(pc->data[i].name, name) == 0){return i;//记录所删除联系人所在位置的下标break;}}if (i == pc->sz)//这里如果判断pos==0时可能是第一个元素因为i是从0开始了所以可以把pos开始赋值为-1{return -1;}
}
void DelContact(Contact* pc)
{char name[MAX_NAME] = { 0 };int i = 0;printf("请输入要删除指定联系人的名字\n");scanf("%s", &name);if (pc->sz == 0){printf("通讯录为空,无法删除\n");}//删除//1.找到删除的指定联系人 - 位置 (下标)int pos = FindByName(pc, name);if (pos == -1){printf("要删除的人不存在\n");}//2.删除 - 删除pos位置上的数据for (i = pos;i<pc->sz -1; i++){pc->data[i] = pc->data[i + 1];}pc->sz--;//元素个数减少printf("删除成功\n");
}
void SearchContact(Contact* pc)
{//我们发现查找也需要遍历删除也需要所以为了简洁我们可以把这个遍历的数组分装成一个函数char name[MAX_NAME] = { 0 };printf("请输入要查找人的名字\n");scanf("%s", &name);int pos = FindByName(pc, name);if (pos == -1){printf("要查找的人不存在\n");}//找到就打印printf("%-10s %-4s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");printf("%-10s %-4d %-5s %-12s %-30s\n",pc->data[pos].name, pc->data[pos].age, pc->data[pos].sex, pc->data[pos].tele, pc->data[pos].addr);
}
void ModifyContact(Contact* pc)
{char name[MAX_NAME] = { 0 };printf("请输入修改指定联系人的名字\n");scanf("%s", &name);int pos = FindByName(pc, name);if (pos == -1){printf("要修改的人不存在\n");}//存在就修改printf("请输入名字>:");scanf("%s", pc->data[pos].name);printf("请输入年龄>:");scanf("%d", &(pc->data[pos].age));printf("请输入性别>:");scanf("%s", pc->data[pos].sex);printf("请输入电话>:");scanf("%s", pc->data[pos].tele);printf("请输入地址>:");scanf("%s", pc->data[pos].addr);printf("修改成功\n");
}
//按照名字排序
int cmp_by_name(const void* e1, const void* e2)
{return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name);
}
void SortContact(Contact* pc)
{qsort(pc->data, pc->sz, sizeof(PeoInfo),cmp_by_name);printf("排序成功\n");
}
//test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"
void meau()
{printf("*****************************\n");printf("****1. add 2. del ****\n");printf("****3. search 4. modify****\n");printf("****5. show 6. sort ****\n");printf("****0. exit ****\n");printf("*****************************\n");
}
enum Option
{EXIT,ADD,DEL,SEARCH,MODIFY,SHOW,SORT
};
int main()
{int input = 0;Contact con;InitContact(&con);do{meau();printf("请选择:>\n");scanf("%d", &input);switch (input){case ADD:AddContact(&con);break;case DEL:DelContact(&con);break;case SEARCH:SearchContact(&con);break;case MODIFY:ModifyContact(&con);break;case SHOW:ShowContact(&con);break;case SORT:SortContact(&con);break;case EXIT:printf("退出通讯录\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
五、一些笔试题
1.调用GetMemory函数的时候,str的传参为值传递,p是str的临时拷贝,所以在GetMemory函数内部讲动态开辟空间的地址存放在p中的时候,不会影响str.所以GetMemory函数返回之后,str中依然是NULL指针。strcpy函数就会调用失败,原因是对NULL的解引用操作,程序会崩溃。
2.GetMemory函数内容malloc申请的空间没有机会释放,造成了内存泄露。
#include<stdio.h>
#include<string.h>
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;
}
返回栈空间地址的问题
GetMemory函数内部创建的数组是临时的,虽然返回了数组的起始地址给了str,但是数组的内存出了
GetMemory函数就被回收了,而str依然保存了数组的起始地址,这时如果使用str,str就是野指针。
#include<stdio.h>
char* GetMemory(void)
{char p[] = "he1lo wor1d";return p;
}
void Test(void)
{char* str = NULL;str = GetMemory();printf(str);
}
int main()
{Test();return 0;
}
总结
Ending,今天的动态内存管理(上)+优化版通讯录+笔试题内容就到此结束啦~,如果后续想了解更多,就请关注我吧,一键三连哦 ~
C语言 --- 动态内存管理(上)+优化版通讯录+笔试题相关推荐
- C语言动态内存管理和动态内存分配函数
给变量分配内存空间可分为静态内存分配和动态内存分配. 静态内存分配属于编译时给变量分配的空间,动态分配属于在程序运行时给变量分配的空间 静态分配属于栈分配,动态分配属于堆分配 运行效率上,静态内存比动 ...
- C语言-动态内存管理(malloc()、calloc()、realloc()、free())
C语言 动态内存分配 文章目录 C语言 动态内存分配 前言 一.为什么存在动态内存分配? 二.动态内存函数的介绍 1.初识malloc()和free() 2.malloc()和free()的简单使用 ...
- C语言动态内存管理和动态内存分配
动态内存管理同时还具有一个优点:当程序在具有更多内存的系统上需要处理更多数据时,不需要重写程序.标准库提供以下四个函数用于动态内存管理: (1) malloc().calloc() 分配新的内存区域. ...
- C语言与JAVA内存管理_C语言动态内存管理和动态内存分配
动态内存管理同时还具有一个优点:当程序在具有更多内存的系统上需要处理更多数据时,不需要重写程序.标准库提供以下四个函数用于动态内存管理: (1) malloc().calloc() 分配新的内存区域. ...
- 【C语言进阶】详解C语言动态内存管理
前言: 今天这篇博客将为大家讲解如何通过开辟动态内存,从而写出更加优秀的的程序.同时今天的内容对于以后想要继续学习c++的同学来说也尤为重要.那就让我们进入正题吧. 一.动态内存概述: 什么是动态内存 ...
- C语言-动态内存管理
C语言中,我们在使用数组的时候,经常有这样的一个问题:数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配.有的时候,我们开辟的空间太小,无法满足我们的需求,有时又太大,浪费空间比较严重. ...
- C++语言动态内存管理介绍和示例
C++动态内存 在C++程序中,所有内存需求都是在程序执行之前通过定义所需的变量来确定的. 但是可能存在程序的内存需求只能在运行时确定的情况.在这些情况下,程序需要动态分配内存,C ++语言将运算符n ...
- C语言动态内存使用多文件实现通讯录,并可以保存在文件中
一.使用多文件实现通讯录 1.头文件"Contact.h" 自己写的头文件:只要写包含库函数的头文件,和自己写的函数的声明,个人信息结构体,通讯录结构体,以及枚举,和宏定义. #p ...
- C语言提高篇之——动态内存管理
目录 1. 动态内存分配存在的意义 2.动态内存函数介绍 2.1 malloc 2.2 free 2.3 calloc 2.4 realloc 3.常见的动态内存错误 3.1 对空指针解引用 3.2 ...
最新文章
- Python编程基础:第十三节 循环控制语句Loop Control Statements
- hive shell/sql 命令行
- 文字滚动的另一方法 拆分文字来做到文字滚动
- 服务器的防火墙禁止了对指定通讯端口的访问,使用iptables限制访问网站指定端口...
- 小汤学编程之JavaScript学习day01——认识JS、JS基础语法
- 2008 读第一本书
- 【Level 08】U06 Good Feeling L6 A 3D experience
- tensorflow conv2d的padding解释以及参数解释
- LeetCode 6 - ZigZag Conversion
- 一步一步安装Git控件版本工具
- scala 主构造器_Scala主构造器深度
- EasyUI-在行内进行表格的增删改操作
- JAVA学生宿舍管理系统
- 安川机器人程序还原_安川机器人程序示例
- table实现radio单选效果
- hdmi接口有什么用_显示器有哪些接口?DP、HDMI、VGA、DVI有什么区别?
- Silverlight 教程第二部分:使用布局管理 (木野狐译) 1
- 3DMAX - 使用编辑多边形的小技巧
- 解决Windows 10不显示打字框
- 微软TTS服务器,微软TTS,Neospeech TTS 简单使用
热门文章
- 网易测试开发岗面试经历
- 无人机避障四种常见技术中,为何大疆首选双目视觉
- [Microsoft][ODBC 驱动程序管理器] 在指定的 DSN 中,驱动程序和应用程序之间的体系结构不匹配
- Linux 批量修改文件名和后缀
- web前端课程设计——动漫网页2个网页HTML+CSS web前端开发技术 web课程设计 网页规划与设计
- JAVA打印月历(以2017年为例)
- caffe特殊层:permute\reshape\flatten\slice\concat
- 1.17 ............
- Android实现画板功能(一)
- Laravel实现软删除