- 博客(42)
- 收藏
- 关注
原创 C++11实现一个自旋锁
自旋锁也是一种互斥锁,和mutex锁相比,它的特点是不会阻塞:如果申请不到锁,就会不断地循环检测锁变量的状态,直到申请到锁。它的核心算法是一个循环检查锁变量的操作,是CPU自我循环的过程,因此称为自旋锁。底层实现有两种算法,一种是把目标值和锁状态变量的原值进行交换,这个过程要求是一个原子操作,然后检查交换出来的值是否是期望的值,称为TAS算法;另一种是先检查锁状态变量是否是期望值,如果是就设置为目标值,并返回成功,这个过程也要求是一个原子操作,称为CAS算法。
2025-04-07 20:55:26
374
原创 为什么函数对象作为函数参数时,一般使用值类型形式?-番外篇
而nmax的第1个参数是回调函数comp的函数指针,第2个参数a是comp的第1个参数,第3个参数b是comp的第2个参数,因此,在nmax中调用comp时,需要把nmax的第2个参数rsi和第3个参数rdx分别赋值到rdi和rsi寄存器中,以符合调用约定,同时第1个参数rdi存放的是comp的函数指针,还得要把这个rdi寄存器腾让出来,以存放comp的第1个参数,因此会有更多的指令来调整这些参数的位置存放。因为要使用rdi寄存器来传递this指针,需要对rdi的原值进行缓存,不得不花费额外的指令。
2025-04-03 16:04:22
859
原创 为什么函数对象作为函数参数时,一般使用值类型形式?
总之,空函数对象作为参数时,在按值和按引用传参时的开销几乎是一样的,无非是按值传参时先不传递函数对象,延迟到在算法函数调用回调函数对象时再临时创建和传递函数对象,而按引用传参时在调用算法函数时就创建了函数对象,无非就是先做还是后做的区别,二者性能几乎完全一样。算法是函数模板,编译器在编译过程中可以看到算法实现的源码,如果在调用时对它进行了内联,同时编译器也知道所设置的回调函数的源码,那么就意味着整个调用链路编译器都是可见的,也就说无论是传递值还是引用,在内联优化展开代码时,没有区别,因此生成了一样的代码。
2025-04-03 13:09:37
680
原创 巧用临时对象之五
这样,如果后续再有读者进行读操作时,通过这个已经更新的全局指针来访问vector就可以了,同样,如果后面再有新的写操作,继续创建一个新的vector对象并进行更新,然后让全局指针指向它。不过,在实践中最好不要使用auto定义变量来接收那个代理对象,如果类型转换时,直接让一个具体类型的变量来接收临时代理对象,如果是调用其它操作符时如“->”,则直接调用,这样代理对象就会自动进行类型转换或者调用其它操作符,因为代理对象是匿名的临时对象,用户也就意识不到是在和一个代理对象打交道,实现的代码看起来也非常干净利索。
2025-04-02 17:13:54
696
原创 编译器眼中的-标量对象和空对象的传参及优化
C++编译器在编译程序时,只要不违背C++语义,可以对程序进行一些变换或者优化。如果C++对象是一些特殊类型的对象,当它们是一个函数的参数(当然也包括成员函数,因为成员函数隐藏着一个this指针,在调用时把对象自己作为参数传递)时,编译器在传递参数时可以进行一些特殊的优化,以提高传递函数参数和访问数据成员的性能。
2025-03-27 18:59:14
1015
原创 编译器眼中的虚函数动态绑定
根据前面的图示,vptr存放在一个对象this指针指向的对象实例中,而对象的this指针的值,只能在运行时才能知道,因此要得到虚函数,那也就得在程序运行的时候了,这个阶段也只能在运行时,无法在编译时静态绑定,所以也就是所谓的动态绑定。综上,动态绑定并不神秘,形象一点说,就是编译时构造了一个绑定虚函数的“公式”,该“公式”在编译的时候确定它的常量系数,在运行的时候得到它的和实际类型相关的动态参数,进行计算后,就能得到实际类型的虚函数指针,是编译器编译时的静态分析和程序运行时的动态查找,它们共同合作的结果。
2025-03-27 16:21:48
525
原创 cmpxchg16b指令的实现分析
我们知道C++11以后提供了原子操作类型:atomic<T>,该原子模板类可以实现原子操作,比如exchange、compare_exchange_weak、fecth_add等,T类型一般都是非常简单的类型,比如整型、指针等类型,这些类型都可以实现指令级的原子操作,所操作的数据类型的长度一般也不能大于内存数据总线的宽度,比如在32/64bit的处理器中,T类型的数据长度不能超过32/64bit。它会比较指针ptr指向的值与给定的旧值oldval,如果相同,则将新值newval写入,并返回真(true);
2025-01-24 19:00:02
938
原创 巧用临时对象(四)
因为它的数据成员都是引用类型,销毁对象时也无需管理资源,过程非常简单,实际上是对临时对象进行了“去皮留瓤”的操作,即在外在形式上,虽然临时对象(皮)销毁后,但在变量x、y、z中却留下了临时对象的各个数据成员的值(瓤),也就是实现了对my_data对象的结构化绑定。那么,我们转换一下思路,把需要绑定的各个独立变量,作为某个类的数据成员,然后重载这个类的赋值操作符operator=,参数是my_data类型,这样,在赋值操作符函数中就可以把my_data对象各个数据成员赋值给每个独立变量了。
2024-12-19 10:55:31
901
原创 巧用临时对象(三)
每当调用sync_shared_ptr的operator->()操作符时,都会创建并返回一个内部类raii的临时对象,因为这个临时对象也重载了操作符->,所以继续调用它的operator->()操作符,最后返回了sync_shared_ptr所保存的裸指针,该指针指向一个vector对象,使用->操作符可以访问它所指向的vector对象的成员函数。对象obj调用locked()函数返回的值被赋值给locked_ptr对象,此时就不再是临时对象了,而是一个具名对象,它的生存期是由作用域决定的。
2024-12-16 16:48:19
597
原创 巧用临时对象之二
对于id来说,因为是基本类型的数据,这点开销并不大,尚可接受,而对于name成员,它是string类型,因为不同库的实现不同,在构造时有的库实现可能需要更多的耗时操作。按照临时对象的生存期定义,它的生存期仅存在于lock_guard<mutex>(source.mtx)表达式所在的这一行,即在调用委托构造函数传递实参时,创建lock_guard的临时对象,并同时进行加锁,当调用返回后表达式生存期结束,这时销毁lock_guard临时对象,并同时进行解锁。
2024-12-14 06:00:21
817
原创 巧用临时对象之一
就以类universal_ptr来说,它只有一个数据成员,而且还是指针类型,指针占用内存非常小,在64位环境下仅占用8字节,而是这个类的析构、拷贝构造和移动构造函数都是缺省的,只是简单的指针赋值操作而已,因此,尽管在调用的中间阶段产生了临时对象,但开销非常小。此外,临时对象使用指针形式的数据成员,也无需担心悬挂指针的问题,因为所创建的临时对象,它即时传参即时创建即时销毁,都是发生在同一个表达式中,它的生存期不会晚于创建时所传递的参数的生存期。时,返回值类型被推导为unique_ptr<string>类型?
2024-12-13 20:20:43
748
原创 探索连续调用多个虚函数的优化
在函数B::foo()中,使用placement new操作符在B类型的对象所占用的内存空间中,创建了它的一个派生类D的对象(二者的sizeof相等,完全可以这样做),因为在调用D的构造函数时,要初始化vptr指针,此时vptr被修改为指向D类的虚函数表,尽管此时B所创建的对象的this指针没有变化,但是因为B和D的对象布局完全一样,this所包含的vptr指向的虚函数表已经被修改成了D的虚函数表。原因是为了安全,尽管虚函数表指针是不可见的,但在程序中仍然可以通过非常规的手段对它进行修改。
2024-11-20 21:49:22
538
原创 栈和局部变量
我们知道程序在运行时,它的内存可以分为数据区、代码区、堆区、栈区等,而栈区是一个线程独占内存区。一个函数分配的局部变量就是在所在线程的栈区分配的,当调用一个函数时,会为它建立一个栈桢,所有函数用到的局部变量都是在这个栈帧中分配的,局部变量所占用的内存空间分配和释放都是自动的,调用函数时随着栈桢的创建,局部变量的空间也就分配好了,当函数退出时释放栈桢,局部变得空间也就释放了,所有的这一切都是自动进行的,无需程序员参与。先看一下栈的运行机制,以x86 32 位 CPU 为例。在刚开始工作在实模式时,CPU中
2024-11-01 10:59:37
572
原创 派生类重载的delete操作符调用时可以动态绑定吗
该函数先调用了dog类的析构函数,然后再调用dog类的重载的operator delete(),正好符合delete操作符的语义,也就是说在这里,编译器使用了一个独立的函数来封装了这个delete操作符的功能。因此,派生类中重载的delete操作符在使用基类指针析构堆上对象时,也是动态绑定来调用的,只不过它并不是使用传统的方式,定义成虚函数来动态绑定的,而是被封装在一个编译器自动生成的虚析构函数中,通过动态绑定虚析构函数来间接的动态绑定。
2024-10-31 15:35:49
879
原创 C++中的CRTP
我们知道,要定义派生类肯定需要知道基类的类型,而在使用类模板来实例化一个模板类时又得需要知道模板的实参类型,在这里实例化基类时的实参类型又是派生类的类型。CRTP基类是类模板,基类实例化之后,和派生类是一一对应的,即一个基类只能派生一个子类,而模板参数化this指针的实现方式是基类只有一个,而且是非模板类,它的派生类可以有多个,但是每定义一个派生类,在该基类中都会使用派生类类型实例化出一个对应的interface()函数,也就是在基类中会有多个interface()成员函数和它的派生类一一对应。
2024-10-18 22:48:33
1527
原创 一种条件语句的编译优化方式
我们知道,指令是以指令流的形式在CPU流水线中解码并执行的,如果指令流的流程没有发生跳转的话,它就会按照指令流中的指令顺序依次执行,并且在前面指令的执行过程中,CPU的解码单元还会预取后面的指令并预先进行译码操作,前面的指令执行完毕紧接着执行已经译码完成的后面的指令,一环扣一环的向前推进。=的比较时,编译器会认为不相等是一个大概率的事件,显然int型的取值范围是0-4G,因此如果a和b的值是均匀分布的话,它们相等的可能性是1/4G,在编译器看来是极低的概率。=b),编译器都生成更有利于a!
2024-10-05 20:38:41
601
1
原创 C++ string类能否被继承?
对于像string这样没有虚函数的类,如果要复用它的功能,首先应该考虑使用对象组合的机制,让string对象是派生类的一个成员,在派生类中通过转发的方式使用string的成员函数。原因是在堆上创建的MyString对象是通过它的基类string类型来delete的,然而string的析构函数并不是virtual函数,这样在delete pStr时,调用的是string的析构函数,并没有调用派生类MyString 的析构函数,从而导致了MyString的数据成员prefix所分配的内存没有被释放。
2024-10-05 16:52:07
1153
原创 当C++遇到空指针异常......
在Java语言中,有空指针异常,在编程时为了代码安全,在遇到空指针时,防止程序崩溃,会捕捉空指针异常,即NullPointerException异常类。比如下面就是一段捕获NullPointerException异常的Java代码片段:try { 。。。。} catch (NullPointerException e) { e.printStackTrace();}当try语句块中的代码访问到空指针后,会抛出NullPointerException,随后在catch语句捕获这个异常,并
2022-04-28 18:24:49
10228
1
原创 C++11实现一个读写自旋锁-3(顺序锁 )
是一种特殊的读写锁,它也是一种乐观锁,我们知道,在读写锁中读写操作之间是互斥的,然而在顺序锁中,读写之间没有锁,写操作的时候无视读者的存在,但是读者在读数据时,要进行校验,验证在读数据期间数据没有被修改过,如果修改了,就放弃已经获取的数据,重新获取数据,也就是说读者总是假定它所读取的数据是正确的,是一种读乐观锁。同lock-free相比,它的关注点是读,只要在读的那一时刻,没有写操作,就认为能够都成功,读完之后再判断在读的过程中是否有写操作发生,如果有,则回滚,重新读。而lock-free更常见的是写操作,
2022-04-28 09:10:11
1320
原创 C++中调用虚函数都是动态绑定吗
多态在面向对象编程中是一个重要的概念,一般是使用虚函数来实现的,原理就是通过虚函数表保存了一个类的虚成员函数的指针,在调用虚函数时,可以通过对象的虚函数表指针来从虚函数表中得到函数指针,因为函数地址是通过函数指针来访问的,所以在编译时是不知道函数入口地址的,只能在运行时通过访问虚函数表来定位。为了方便分析说明,先定义几个有继承关系的类:// 基类class base {public: base() {} base(int) { foo(); } virtual void f
2022-03-17 17:36:19
1342
原创 C++中值语义的函数参数和返回值的背后
我们知道,在C++中对象作为数据传递时,如函数的参数传递和返回值,有值语义和引用语义的形式之说。当使用基本数据类型(如int、short、bool、指针、引用)进行传递时,实现非常简单,因为它们的值可以直接放入寄存器中,把寄存器作为参数传递和返回值的载体,就可以了,从一些函数调用约定就知道这些情况。可是,如果要传递的数据不是基本类型,而是用户定义的类型,比如class、struct等复合数据类型,这些类型的数据是无法放入一个寄存器的,那么在C++中是如果实现的呢。比如,我们有一个类,如下:class.
2022-03-17 11:26:32
1097
原创 C++11实现一个读写自旋锁-2
方案2class rw_spin_lock {public: rw_spin_lock() = default; ~rw_spin_lock() = default; rw_spin_lock(const rw_spin_lock&) = delete; rw_spin_lock &operator=(const rw_spin_lock&) = delete; void lock_reader() noexcept; void unlock_reader()
2021-12-21 10:21:33
329
原创 C++11实现一个读写自旋锁-1
本文介绍两种使用自旋锁方式实现读写锁的方案。方案一基本原理是使用一个原子变量作为计数器,如果该计数器的值大于0,说明有读者在持有读锁,它的值就是读者的数量,当计数器的值为-1时,说明有写者在持有写锁,如果计数器的值为0,则说明既没有读者持有读锁,也没有写锁持有写锁。所提供的接口有申请读锁,尝试申请读锁,释放读锁,申请写锁,尝试申请写锁,释放写锁等。定义读写锁rw_spin_lock类如下,它包含一个atomic_int类型的数据成员和六个申请锁、释放锁的成员函数,没有拷贝和移动语义。在实现时,要注意申
2021-12-16 21:34:08
1854
原创 C++11实现一个cyclic barrier
举个生活中的例子,假如有5个好基友,商量好周末一块去爬山,他们约定周日早上8点在山脚下集合,不见不散。周日那天,如果一个先到了,发现没有其他人到达,就只好等着,第二个人到了之后,发现还没有到期,也只能等待,直到第5个人到达后,所有5个人全部到齐了,就一起出发开始爬山。如果我们在脑海中想象有一个栅栏(barrier)立在山门口,如果它不拿走,人们是无法越过去的,它被拿走的条件是,5个基友全部到达。如果这5个基友哪怕仅有一个还没有到达,它也不会被放倒,基友就被拦在外面,只有当5个基友全部到达之后,这个栅栏才会陡
2021-12-15 21:26:26
1473
原创 C++11实现一个countdown latch
在日常生活中,我们经常遇到这样场景,当要做一件事情时,要先等待几个固定数目的其它事情做完了,才能进行,如果别的事情没有就绪,只能等待。比如,一个工厂有存放原材料的仓库,仓库的大门共有三把锁,分别由仓管员、主管部门经理和值班经理保管,当一个(或几个)领料员上班后去仓库领材料,发现仓库大门锁着,那么他就只能等着这三个掌管钥匙的人员来开门:如果主管部门经理来上班了,就把他负责的那把锁打开,然后去工作了,此时领料员只能继续等待,当值班经理来上班之后,把他负责的那把锁打开,然后也去工作了,只有最后仓管员上班之后打开最
2021-12-12 21:15:15
3342
原创 浅议C++回调函数的实现方式
引子-模板方法模式的实现看一下设计模式中模板方法模式的实现方式,首先定义一个模板基类,它有一个模板成员函数和一个钩子成员函数:class TemplateBase { int data; // 假设有一个int型的数据成员public: virtual ~TemplateBase() {} void process() { // 模板函数 // 其它准备操作 hook(); // 其它后续操作 } virtual void hook() {} // 钩子函数 // 其它
2021-11-30 09:34:47
1600
1
原创 智能指针之unique_ptr
unique_ptr用于独占它所指向的对象。某个时刻只能有一个unique_ptr指向一个给定的对象,也就是这个对象不会被多个unique_ptr同时共享,它只提供了移动语义,即它所管理的资源对象只能在unique_ptr之间进行移动,不能拷贝。资源对象的生命周期被唯一的一个unique_ptr托管着,一旦unique_ptr被销毁或者变成empty对象,或者拥有了另一个资源对象,它先前拥有的对象同时一并销毁,显然一旦对象离开unique_ptr的管理范围就会销毁,保证了内存不会泄露。下面看一下它的用法。
2021-11-12 16:45:25
14133
1
原创 构造函数和析构函数中调用虚函数是多态吗
在面向对象编程语言中,一个类的构造函数和析构函数是两个比较特殊的成员函数,它们主要用于对象的创建和销毁,和对象的生命周期息息相关,因此它们有着特殊的含义。编译器对待它们和其它普通的成员函数不一样,在编译它们时会添加一些额外的代码来做一些专门用途的业务逻辑。本文介绍其中一个和虚函数调用有关的场景及其实现机制。我们知道,C++为了实现面向对象的多态语义,设计了虚函数机制,具体地说,就是每个带有虚函数的类都会有一个虚函数表vtbl,在里面存放了各个虚函数的调用入口地址,在创建对象时,会为对象分配一个虚函数指针v
2021-11-09 16:50:59
847
原创 自旋锁的实现及优化
自旋锁的实现算法大多使用的是Test And Set算法,简称TAS,也就是先对目标值进行检查,如果目标符合预期的要求则同时把它修改为所需要的值。先介绍一下TAS原语,它的语义原型如下:function tas(p : pointer to bool) returns bool { bool value = *p if !*p { *p ←true } return value}它的语义是这样的:如果指针p指向的变量原值为false,就设置
2021-11-04 17:17:30
5028
3
原创 双重检查锁与单例模式
单例模式是比较常见的一种设计模式,它的实现方式有很多种,曾经见过一篇文章中列了十几种实现方式,比如饿汉式、懒汉式、双重检查锁、枚举。。。等等,大家也应该非常熟悉常见的实现方式,本文简单的谈谈其中的“双重检查锁”实现方式。我们知道单例模式的对象在进程中仅有一份,在多线程环境下为了防止创建出多个对象,需要对创建对象的过程进行互斥操作,这样,当多线程同时竞争时,保证只能由一个线程来创建唯一对象。互斥操作方式常见的就是锁,比如互斥锁或者自旋锁。下面的C++代码片段就是使用mutex锁来实现的,原理大家都明白,就不
2021-10-26 18:38:09
2486
原创 Android中的设计模式-职责链模式
在介绍该模式之前,先提一个问题,下图是SQL语句select执行时结果集的流转图,如果让你编写解析该select语句的代码,你会怎样设计方案呢?因为在select语句中,有些字段不一定出现,如group、limit、where等,因此,肯定不能按照固定的语句格式来解析,最常见的方式可能就是,使用大量的if。。else if。。else if。。else。。语句,根据select中实际出现的字段去调...
2018-05-22 13:38:55
468
原创 Android中的设计模式-享元模式
我们在编程实践中,经常会遇到这样的场景:许多类或方法中,用到了一些代码模块,这些代码的逻辑结构完全一样。那么我们一般会对这些代码进行重构,把这些代码抽取出来,组成一个新的方法,并把它放到基类或者工具类中,作为通用方法,这样各个不同的类或方法能同时使用它们来完成功能,显然,这种方式提高了代码的复用性,减少了代码的数量。这个场景就是典型的享元模式场景。该模式的定义非常简单: 运用共享技术有...
2018-05-18 14:24:59
637
原创 Android中的设计模式-桥梁模式
“假舆马者,非利足也,而致千里;假舟楫者,非能水也,而绝江河。君子生非异也,善假于物也。”——荀子《劝学》。 美国好莱坞电影有《蜘蛛侠》、《蝙蝠侠》,无非就是让人具有了某种动物的能力,从而“能力越大、责任越大”,如果换成面向对象术语的话,就是“人”类继承了“蜘蛛”类和“蝙蝠”类,从而具有了它们的功能。按照这个思路发展,如果人类要想渡海过江,就得继承鱼类,人类要想远行,就得继承马类,于是电影《鲨鱼...
2018-05-16 14:49:10
647
1
原创 Android中的设计模式-命令模式
两位大侠均系出名门,适配器大侠身为结构教长老,而策略大侠位居行为派护法。二侠虽然门派不同,素昧平生,却也一见如故,把酒言欢之余,心意相通,准备合奏一曲助兴。只见适配器大侠使出乾坤大挪移,无论何种乐器,或吹、或拉、或弹、或敲,不管音调如何,或轻、或重、或缓、或急,都被他一一化解为一种和音,无不处处落在策略大侠的旋律上。丝丝入扣,不差毫厘,冥冥之中自有天意,“适配器”大侠与“策略”大侠竟然合成了一首《...
2018-05-16 13:10:27
340
原创 Android中的设计模式-备忘录模式
公元1722年,康熙皇帝驾崩于北京畅春园,步军统领隆科多取出了藏在正大光明匾额后面的遗诏,宣布四阿哥胤禛克承大统,继承皇位,天朝帝国从此走进了新时代。康熙为什么采用遗诏,而不是自己宣布继承人?还不是为了防止出现意外:躲猫猫、马航370、相亲遇到翟欣欣、被闺蜜锁在门外…;别人能够知道和篡改遗诏的内容吗?别人敢吗?!这样康熙使用遗诏的方式,在龙驭宾天后,恢复了“皇帝类”的另一个对象实例:雍正。康熙爷在...
2018-05-16 10:33:23
907
原创 Android中的设计模式-观察者模式
沛公军霸上,未得与项羽相见。沛公左司马曹无伤使人言于项羽曰:“沛公欲王关中,使子婴为相,珍宝尽有之。”项羽大怒曰:“旦日飨士卒,为击破沛公军!”……项伯乃夜驰之沛公军。——《史记·项羽本纪》 曹无伤是项羽安插在刘邦身旁的“卧底”,也就是项羽向刘邦注册的一个“观察者”,在“主题”刘邦状态改变时:“沛公欲王关中”,向“订购者”项羽发送通知:“使人言于项羽”。 不过,虽然鸿门宴上杀机重重,但刘邦最后...
2018-05-15 14:33:47
247
原创 Android中的设计模式-模板方法模式
“浓眉大眼好干部,尖嘴猴腮狗特务,好人机枪打不死,坏蛋一枪就玩完!”,拍摄革命题材电影是有套路(模板)的,每当共产党员中枪了,要牺牲在战友怀中的时候,我们知道此时该有经典场景了:有的电影是“部队和群众都安全转移了吗?”,有的电影是“这是我的党费”,。。。不管哪种形式,用在这个场景肯定合适,然后就安详地闭上了双眼。这就是模板方法模式在电影中的应用,它是属于行为型: 在一个方法中定义一个算...
2018-05-15 14:16:19
586
原创 Android中的设计模式-策略模式
大道至简,越是简单就越接近事物的本质。策略模式是非常简单的一个模式,属于行为型模式。 定义一系列的算法,把它们一个个封装起来,而且使它们可以相互替换。本模式使得算法可独立于使用它的客户而变化。下面是它的结构类图: - 策略接口(Strategy):定义所有算法的公共接口。Context使用这些接口来调用具体的策略类。 - 具体的策略实现(Concrete Strate...
2018-05-15 13:29:16
535
原创 Android常见优化方式-避开高峰期
现在随着私家车越来越多,交通阻塞成为家常便饭,为了缓解交通压力,一些城市采取了交通限行方式,比如北京禁止外地牌照的汽车在工作日期间早晚高峰期进入五环,等到车流高峰期过去之后再上路。如果把Android应用程序比作是一座城市的话,把CPU和内存等资源看作是城市的交通道路,把组成应用程序的各个功能模块,比如UI、后台服务、等看作是汽车,它们运行的时候看作是汽车上路行驶。那么我们编程实践时,...
2018-05-15 12:34:43
333
原创 Android常见架构模式-依赖注入模式
我们来设想一下使用Android框架来实现UI界面,应该是什么样的过程。正常的思路可能是这样的:首先要为Android框架定义一个派生类,如Activity的子类,或者实现一个接口类,如View类定义的OnXxxListener()接口类;其次,创建这些类的对象实例;接着,把对象实例注册到框架中去;最后,由框架根据需要调用这些应用层实现的接口方法。涉及到应用层的操作共有三个步骤:1、定义类...
2018-05-15 10:20:49
1402
空空如也
空空如也
TA创建的收藏夹 TA关注的收藏夹
TA关注的人