C++的基础知识【面试遇到】
文章目录
- 1. 多态
- 1.1 多态的分类
- 1.2 动态多态满足的条件及使用
- 1.3 动态多态:虚函数
- 1. 虚函数:
- 2. 虚析构函数:
- 3. 纯虚函数:
- 4. 虚函数与纯虚函数:
- 5.虚函数指针与虚函数表
- 2. 空指针与野指针
- 2.1 空指针
- 2.2 野指针
- 3. const那些事
- 3.1 const修饰指针
- 3.2 const修饰函数
- 3.3 const修饰函数
- 4. static那些事
- 5. class与struct的区别
- 6. 构造函数与析构函数
- 6.1 构造函数
- 6.1 析构函数
- 7. 深拷贝与浅拷贝
- 8. 模板
- 8.1 小结:
- 8.2 使用模板的注意事项
- 8.3 普通函数与模板函数的区别
- 9. 动态库与静态库
- 9.1 静态库(.lib)
- 9.2 动态库(.dll)
- 9. 指针与引用
- 10. 手动实现string类
- 10. 智能指针
- 10.1 C++98的auto_ptr
- 10.1 C++11的智能指针
- 1. unique_ptr的使用
- 2. shared_ptr的使用
- 3. weak_ptr的使用
- 11. STL底层实现
- 11.1 STL容器类型
- 11.2 使用注意事项
- 11.3 容器特性
- 12. new和malloc的区别
- 1. malloc、calloc、realloc、alloca
- 2. malloc与new
- 13. 内存泄漏、内存溢出与野指针
- 14. 指针函数与函数指针
- 1. 指针函数
- 2. 函数指针
- 14. Struct结构体大小计算
- 15. C++的5大存储区
- 16. 传值、传引用、传指针
1. 多态
参考:参考视频
1.1 多态的分类
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名,但是形参的个数或类型不同
- 动态多态:派生类和虚函数实现运行时多态
- 静态多态与动态多态的区别:
- 静态多态的函数地址早绑定,编译阶段确定函数地址
- 动态多态的函数地址晚绑定,运行阶段确定函数地址
1.2 动态多态满足的条件及使用
条件:
- 有继承关系
- 子类重写父类的虚函数【重写即函数返回类型,函数名,形参列表均相同】
使用:
- 父类的指针或引用 指向子类的对象
1.3 动态多态:虚函数
1. 虚函数:
- 使用virtual修饰的函数,如
virtual double calcArea();
- 注意:1)普通函数(非成员内函数)不能是虚函数;2)静态函数(static)不能是虚函数;3)构造函数不能是虚函数;4)内联函数不能是表现多态性时的虚函数
2. 虚析构函数:
- 虚析构函数为了避免内存泄漏,使得在删除指向子类对象的基类指针时可以调用子类的析构函数。【在调用父类的虚析构函数时,会先调用子类的析构函数,再调用父类的析构函数,以此来防止内存泄漏】
3. 纯虚函数:
- 纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。如:
virtual int A() = 0;
4. 虚函数与纯虚函数:
- 虚函数是被实现的,可以空实现,作用是为了在子类里被重写;虚函数只是个函数声明,留到子类里去实现,作用是一个借口。
- 在子类中,虚函数可以不被重写,但是纯虚函数必须实现后,才可以实例化子类
- 带虚函数的类叫抽象类,不能实例化,只有被继承并实现后才可以使用。抽象类被继承后,即可以是抽象类,也可以是普通类
5.虚函数指针与虚函数表
- 虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
- 虚函数表:在程序只读数据段,存放虚函数指针。如果派生类实现了基类的虚函数,则会类似继承基类的属性一样,继承虚函数表,同时将实现的虚函数指针替换基类的虚函数指针。
2. 空指针与野指针
2.1 空指针
空指针:指针变量指向内存编号为0的空间
用途:初始化指针变量
注意:空指针指向的内存空间不可以被访问
int * p = NULL;
count << *p << endl; // 报错,因为空指针所指向的内存不可以被访问
int a = 10;
p = &a; // 可以用来赋值
2.2 野指针
指针变量指向非法内存空间
//指针p指向内存地址编号为0x1100的空间,由于不知道地址编号为0x1100的空间的具体情况,对此空间的数据进行操作,会报错
int * p = (int *)0x1100;
count << *p << endl; //报错
3. const那些事
- 常量指针:地址内的内容不可改,指针方向可以改【const int * a】
- 指针常量:指针的指向不可改,地址的内容可以改【int * const a】
- 常成员函数:不得修改类内变量,若想修改,变量前加mutable。【int fuc() const】
- 常对象:只能调用常函数【const A a】
3.1 const修饰指针
- const修饰指针 ——常量指针:指针的指向可以改,指针指向的值不可以改
const 在*前面,所以叫常量指针;*p(指针指向的值)不可以改,但是可以改指针的指向
int a = 10, b = 20;
const int * p = &a; //const 在*前面,所以叫常量指针;*p(指针指向的值)不可以改,但是可以改指针的指向(p)
*p = 20; // 报错
p = &b; // ok
- const修饰常量 ——指针常量:
const 在p前面,所以叫指针常量;p(指针的指向)不可以改,但是可以改指针指向的值 (*p)
int a = 10, b = 20;
int * const p = &a; //const 在p前面,所以叫指针常量;p(指针的指向)不可以改,但是可以改指针指向的值(*p)
*p = 20; // ok
p = &b; // 报错
- const即修饰指针,又修饰常量
int a = 10, b = 20;
const int * const p = &a;
*p = 20; // 报错
p = &b; //报错
3.2 const修饰函数
- 常函数:
- 成员函数后加入const后,我们称这个函数为常函数
- 常函数内不可以修改成员的属性
- 成员属性声明时,加入mutable后,在常函数中依然可以更改
- 常对象:
- 声明对象前加入const称该对象为常对象
- 常对象只能调用常函数
class Person {mutable int a = 10; //若想在常函数中修改属性值,需要加上mutable;int b = 10;//常函数中this指针其实为 const int * const this 即不可以修改this的所指地址//也不可以修改this所指地址的值。void make1 () const{this->a = 20; //okthis->b = 20; //报错this = NULL; //报错}void make2 () {this->a = 20; //okthis->b = 20; //okthis = NULL; //报错}
};const Person p; //常对象只能调用常函数
p.make1(); //ok
p.make2(); //报错
3.3 const修饰函数
4. static那些事
- 修饰普通变量:修改变量的存储区域和生命周期,使变量从局部变量移至静态区,生命周期至代码运行结束消亡。在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
- 修饰普通函数:表明函数的作用范围,尽在定义函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
- 修饰成员变量:static修饰的成员变量,所有的对象只保存(共享)一个该变量,且不需要生成对象就可以访问该成员
- 修饰成员函数:static修饰的成员函数,不能访问非静态成员,且不需生成对象就可以访问。
5. class与struct的区别
参考:参考视频
唯一区别在于默认的访问权限不同
struct默认权限为公共(public)
class默认权限为私有(private)
class C1
{int m_A; //默认为私有权限
}
struct C2
{int m_a; //默认为公共权限
}
int main()
{C1 c1;c1.m_A = 10; //报错,因为c1默认的m_A为私有,不可访问C2 c2;c2.m_A = 10; //ok
}
6. 构造函数与析构函数
- 构造函数主要用来在创建对象时为对象的成员属性赋初值,一般由编译器自动调用,无须手动调用
- 析构函数主要用在对象销毁前自动调用,执行一些清理工作
6.1 构造函数
- 形式:类名(){} //函数名与类名相同
- 构造函数没有返回值也不写void
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时会自动调用,不用手动调用
6.1 析构函数
- 形式:~类名(){} //函数名与类名相同,在前面加个
- 构造函数没有返回值也不写void
- 构造函数不可以有参数,因此不可以发生重载
- 程序在调用对象时会自动调用,不用手动调用
- 用途:可以用来释放在堆中开辟出的内存
7. 深拷贝与浅拷贝
参考:参考视频
浅拷贝:简单的赋值拷贝操作【编译器所实现的拷贝都属于浅拷贝】
深拷贝:在堆中重新申请空间,进行拷贝操作
注:如果属性中有在堆区开辟的,一定要重写构造函数,进行深拷贝,解决浅拷贝可能会重复释放内存导致报错的问题
8. 模板
参考:参考视频
8.1 小结:
- 函数模板利用关键字 template
- 使用函数模板有两种方式:自动类型推导、显示指定类型
- 模板的目的是为了提高复用性,将类型参数化
- 模板不是通用的,但是可以利用具体化的模板,解决自定义类型通用化的模板【比如自定义一个Person类,比较p1 和p2,因为是自定义类没有定义比较规则,所以直接使用模板<T=Person>会报错,可以自定义一个具体化模板来解决此问题。参考】
//函数模板
template<typename T> //声明一个模板,告诉编译器后面代码中的T不要报错
void mySwap(T &a, T &b)
{T temp = a;a = b;b = temp;
}
int main()
{int a = 10, b = 20;mySwap(a, b); //调用模板方式1:自动类型推导//mySwap<int>(a, b); //调用模板方式1:显示指定类型;< >内为T所应该对应的类型
}
8.2 使用模板的注意事项
参考:参考视频
- 自动类型推导,必须推导出一致的数据类型T,才可以使用
- 模板必须要确定出T的数据类型,才可以使用
8.3 普通函数与模板函数的区别
- 普通函数可以进行隐式自动转换;【隐式自动转换:比如实际参数是char型,函数需要int型参数,就可以自动转化数据类型,将char转成int】
- 自动类型推导模板函数,不可以进行隐式自动转换;
- 显示指定类型模板函数,可以进行隐式自动转换;
9. 动态库与静态库
两者区别:
- 静态库会包含在.exe中,不会又很多额外文件,但会导致.exe文件过大;
- 动态库与.exe分离,因此会在.exe同级产生很多.dll文件,但同一个.dll可以被不同的.exe调用,增加了复用性。
9.1 静态库(.lib)
- 配置调用静态库,需要设置3个地方,参考
- 编写静态库:
- 头文件:定义函数名
- .cpp文件:函数功能具体实现
- 打包静态库:编译.cpp文件,在debug文件夹中会生成.lib文件。创建lib文件夹,将.lib文件放入;创建include文件,将.h文件放入,静态库大致就封装好了参考
9.2 动态库(.dll)
- 配置调用动态库,也需要设置3个地方,参考
- 编写动态库:
- 头文件:定义函数名,额外需要设置一些宏
- cpp文件:函数功能具体实现
- 打包动态库:编译.cpp文件,在debug文件夹中会生成.dll文件,借助dependcy软件处理.dll文件,使得别人可以灵活调用。参考
9. 指针与引用
参考:C++指针和引用及区别
引用是一种特殊的指针,它在用时自动解引用
- 指针有自己的一块空间,而引用只是一个别名(与原变量共享空间);
- 使用sizeof看一个指针是4,引用是被引用对象的大小;
- 指针可以初始化为NULL, 而引用必须被初始化且必须是一个已有对象的引用;
- 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
- 针可以有多级指针(p),而引用只有一级**;
- 指针和引用使用++运算符的意义不一样;
10. 手动实现string类
- string的底层也是 char*
参考1
参考2
class String
{public://构造函数String(const char* str = NULL);//析构函数~String();
private:char * m_data;
};String::String(const char* str)
{if (str == NULL) {m_data = new char[1];*m_data = '\0';}else {m_data = new char[strlen(str)+1];strcpy(m_data, str);}
}
String::~String()
{if (m_data != NULL) {delete[] m_data;m_data = NULL;}
}
10. 智能指针
参考
10.1 C++98的auto_ptr
对裸指针进行封装,让程序员无需手动释放指针指向的内存区域,在auto_ptr生命周期结束时自动释放,然而,由于auto_ptr在转移指针所有权后会产生野指针,导致程序运行时crash,如下面示例代码所示:
auto_ptr<int> p1(new int(10));
auto_ptr<int> p2 = p1; //转移控制权
*p1 += 10; //crash,p1为空指针,可以用p1->get判空做保护
10.1 C++11的智能指针
- C++11推出了智能指针unique_ptr、shared_ptr、weak_ptr
1. unique_ptr的使用
unique_ptr是auto_ptr的继承者,对于同一块内存只能有一个持有者,而unique_ptr和auto_ptr唯一区别就是unique_ptr 不允许赋值操作,也就是不能放在等号的右边(函数的参数和返回值例外),这一定程度避免了一些误操作导致指针所有权转移,然而,unique_str依然有提供所有权转移的方法move,调用move后,原unique_ptr就会失效,再用其访问裸指针也会发生和auto_ptr相似的crash,如下面示例代码,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指针所有权被转移。
unique_ptr<int> up(new int(5));
//auto up2 = up; // 编译错误
auto up2 = move(up);
cout << *up << endl; //crash,up已经失效,无法访问其裸指针
2. shared_ptr的使用
shared_ptr 使用引用计数,实现对同一块内存可以有多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和unique_ptr最大的区别。
3. weak_ptr的使用
- 使用shared_ptr过程中有可能会出现循环引用,关键原因是使用shared_ptr引用一个指针时会导致强引用计数+1,从此该指针的生命周期就会取决于该shared_ptr的生命周期,然而,有些情况我们一个类A里面只是想引用一下另外一个类B的对象,类B对象的创建不在类A,因此类A也无需管理类B对象的释放,这个时候weak_ptr就应运而生了,使用shared_ptr赋值给一个weak_ptr 不会增加强引用计数(strong_count),取而代之的是增加一个弱引用计数(weak_count),而弱引用计数不会影响到指针的生命周期,这就解开了循环引用.
- 弱指针允许你共享但不拥有某个对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)
11. STL底层实现
参考:STL底层介绍
11.1 STL容器类型
- 连续内存的容器:vector 、deque
- 基于节点的容器:list、set、multiset、map、multimap
- 各容器的数据模型:
vector(数组) 、list(链表)、deque(数组和链表的折中)、set(二叉树)、multiset(二叉树)、map、multimap(二叉树)
11.2 使用注意事项
- 需要大量添加元素时:
vector 、deque因为是连续存储,插入中间位置时,需要拷贝大量信息,所以不适用;而list底层是链表实现,都是常数时间的插入消耗,所以list更合适 - 查找速度:
序列容器:
1)已排序的序列容器,可以使用二分查找等方法,查找速度为O(logN);
- 未排序的序列容器,查找速度为线性的O(n)
对于关联容器,存储的时候存储的是一棵红黑树,查找速度为O(logN);
- 内存是否和C兼容:只有vector可以支持
11.3 容器特性
12. new和malloc的区别
1. malloc、calloc、realloc、alloca
- malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
- calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。
- realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
- alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。
2. malloc与new
- malloc与free是C++/C语言的bai标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
- 对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
- 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
- C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
- new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。
13. 内存泄漏、内存溢出与野指针
参考:C++的内存泄漏、溢出、野指针
- 内存泄漏:
内存泄漏是指我们在堆中申请(new/malloc)了一块内存,但是没有去手动的释放(delete/free)内存,导致指针已经消失,而指针指向的东西还在,已经不能控制这块内存。使用完这个变量之后却没有及时回收这部分内存,这时我们就说发生了内存泄露。如果发生了内存泄露又没有及时发现,随着程序运行时间的增加,程序越来越大,直到消耗完系统的所有内存,然后系统崩溃。
- 内存溢出:
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
- 野指针:
野指针是指向一个已删除的对象或未申请访问的内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。
a. 指针变量未初始化
b. 野指针指针释放后之后未置空
c. 野指针指针操作超越变量作用域(不要返回指向栈内存的指针,因为栈内存在函数结束时会被释放)
14. 指针函数与函数指针
参考:函数指针和指针函数用法和区别
1. 指针函数
- 声明格式:类型标识符* 函数名(参数表) 如:
int *fun(int x,int y);
- 指针函数的返回值是一个指针;
2. 函数指针
- 声明格式:类型标识符 (*函数名)(参数表) 如:`int (*fun)(int x, int y);
- 函数指针是把一个函数的地址赋值给它,再通过调用地址内的函数去运算,感觉和正常的调用函数一样
//函数赋值给函数指针的两种方法
fun = &Function;
fun = Function;
//调用函数指针的两种方法
x = (*fun)();
x = fun();
14. Struct结构体大小计算
- CPU周期: WIN vs qt 默认8字节对齐、Linux 32位 默认4字节对齐,64位默认8字节对齐
- 结构体最大成员(基本数据类型变量)
- 预编译指令#pragma pack(n)手动设置 n–只能填1 2 4 8 16
计算规则:
- 实际对齐单位:取上述3者最小的
- 除结构体的第一个成员外,其他所有的成员的地址相对于结构体地址(即它首个成员的地址)的偏移量必须为实际对齐单位或自身大小的整数倍(取两者中小的那个)
- 结构体的整体大小必须为实际对齐单位的整数倍。
15. C++的5大存储区
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区
1.栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
2.堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
3.自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
4.全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
5.常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)
16. 传值、传引用、传指针
参考:传值和传引用、传指针的区别
- 传指针本质上是传值的一种。传值与传指针,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本(所以如果参数传递数据较大时,还将调用拷贝构造函数,比较耗时,建议传引用),对形参的改变不会影响实参,但是如果传指针时,对形参(指针)指向内容的改变还是会有作用的。
- 传引用的话,形参与实参是同一个对象,只是他们名字不同而已, 对行参的修改将影响实参的值。
- 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差
C++的基础知识【面试遇到】相关推荐
- mysql系列问答题_(2)MySQL运维基础知识面试问答题
面试题001:请解释关系型数据库概念及主要特点? 面试题002:请说出关系型数据库的典型产品.特点及应用场景? 面试题003:请解释非关系型数据库概念及主要特点? 面试题004:请说出非关系型数据库的 ...
- 软件测试基础知识面试题目(25题英文题目)
软件测试基础知识面试题目(25题英文题目) 1. Verification is: a. Checking that we are building the right system b. Chec ...
- [C/C++基础知识] 面试再谈struct和union大小问题
最近找工作参加了很多笔试,其中考察结构体和联合体的大小问题是经常出现的一个问题.虽然题目简单而且分值比较低,但是还是想再给大家回顾下这些C和C++的基础知识.希望文章对你有所帮助~ P ...
- Hadoop之Hadoop基础知识面试复习
Hadoop之Hadoop基础知识常问面试题 列举几个hadoop生态圈的组件并做简要描述. Zookeeper:是一个开源的分布式应用程序协调服务,基于zookeeper可以实现同步服务,配置维护, ...
- java基础知识面试_Java 基础面试知识点
Java 基础知识相关 Java中 == 和 equals 和 hashCode 的区别 对于关系操作符 == 若操作数的类型是基本数据类型,则该关系操作符判断的是左右两边操作数的值是否相等 若操作数 ...
- java总结(基础知识-面试)
lJava发射(案例) l反射含义: lJAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态 ...
- Web 前端基础知识面试大全
目录 一.HTML 1.对 HTML 语义化的理解 2.区别:src 和 href 3.DOCTYPE 的作用 4.HTML5 的新特性 5.script 标签中的 defer 和 async 6. ...
- 基础知识 + 面试题目 总结 索引页
1 网络编程 同步.异步.阻塞.非阻塞 http://www.cnblogs.com/diegodu/p/3977739.html 2 TCP http://calvin1978.blogcn. ...
- MySQL基础知识面试选择题40
1.数据库系统的核心是_B_. A.数据库 B.数据库管理系统 C.数据模型 D.软件工具 2.SQL语言包括_ABCD_.(多选) A.DCL B.DML C ...
- 前端中高级基础知识面试汇总
持续更新ing- 前端基础github地址.README.md可以下载到typora中打开,会有整个大纲目录显示(github中markdown目录快捷生成方式不现实,之后可能会想办法生成贴过来,暂时 ...
最新文章
- JAVA8 获取叶节点_Java找出所有的根节点到叶子节点的节点值之和等于sum 的路径...
- Java--缓存热点数据,最近最少使用算法
- python pandas爬取网页成绩表格,计算各个类别学分
- STL源码剖析 读书笔记一 2013-5-4
- 博客园速度太快了,快得让人心慌……
- Android:SQLiteOpenHelper数据库的两套API
- Atitit 文档资料管理同步解决方案
- js面向对象练习(二):JS面向对象的思路(canvas)写躁动的小球
- CDATA不支持html,我应该在HTML5中使用(Should I use in HTML5?)
- linux date命令 下月,Linux date命令用法和使用技巧(获取今天.昨天.一分钟前等)
- android自定义pickerview,开源项目 好用的PickerView库了
- 更改Web应用地址栏显示的图标
- 不同IP网段连接网络打印机
- xiuno开发文档_$ip-XiunoPHP 4.0 开发手册
- StringBuilder和输入输出
- Cast方法oracle,oracle 中cast方法的使用
- 进程和线程的区别 进程间的通信方式
- 一款基于SpringBoot开发开源外卖系统
- 我的爸爸,在滴滴做自动驾驶
- windows桌面便笺使用小技巧