一文詳解C++ 智能指針的原理、分類及使用
1. 智能指針介紹
為解決裸指針可能導(dǎo)致的內(nèi)存泄漏問題。如:
a)忘記釋放內(nèi)存;
b)程序提前退出導(dǎo)致資源釋放代碼未執(zhí)行到。
就出現(xiàn)了智能指針,能夠做到資源的自動(dòng)釋放。
2. 智能指針的原理和簡單實(shí)現(xiàn)
2.1 智能指針的原理
將裸指針封裝為一個(gè)智能指針類,需要使用該裸指針時(shí),就創(chuàng)建該類的對象;利用棧區(qū)對象出作用域會(huì)自動(dòng)析構(gòu)的特性,保證資源的自動(dòng)釋放。
2.2 智能指針的簡單實(shí)現(xiàn)
代碼示例:
template<typename T> class MySmartPtr { public: MySmartPtr(T* ptr = nullptr):mptr(ptr) { // 創(chuàng)建該對象時(shí),裸指針會(huì)傳給對象 } ~MySmartPtr() { // 對象出作用域會(huì)自動(dòng)析構(gòu),因此會(huì)釋放裸指針指向的資源 delete mptr; } // *運(yùn)算符重載 T& operator*() { // 提供智能指針的解引用操作,即返回它包裝的裸指針的解引用 return *mptr; } // ->運(yùn)算符重載 T* operator->() { // 即返回裸指針 return mptr; } private: T* mptr; }; class Obj { public: void func() { cout << "Obj::func" << endl; } }; void test01() { /*創(chuàng)建一個(gè)int型的裸指針, 使用MySmartPtr將其封裝為智能指針對象ptr,ptr對象除了作用域就會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù)。 智能指針就是利用棧上對象出作用域自動(dòng)析構(gòu)這一特性。*/ MySmartPtr<int> ptr0(new int); *ptr0 = 10; MySmartPtr<Obj> ptr1(new Obj); ptr1->func(); (ptr1.operator->())->func(); // 等價(jià)于上面 /* 中間異常退出,智能指針也會(huì)自動(dòng)釋放資源。 if (xxx) { throw "...."; } if (yyy) { return -1; } */ }
3. 智能指針分類
3.1 問題引入
接著使用上述自己實(shí)現(xiàn)的智能指針進(jìn)行拷貝構(gòu)造:
void test02() { MySmartPtr<int> p1(new int); // p1指向一塊int型內(nèi)存空間 MySmartPtr<int> p2(p1); // p2指向p1指向的內(nèi)存空間 *p1 = 10; // 內(nèi)存空間的值為10 *p2 = 20; // 內(nèi)存空間的值被改為20 }
但運(yùn)行時(shí)出錯(cuò):
原因在于p1和p2指向同一塊int型堆區(qū)內(nèi)存空間,p2析構(gòu)將該int型空間釋放,p1再析構(gòu)時(shí)釋放同一塊內(nèi)存,則出錯(cuò)。
那可否使用如下深拷貝解決該問題?
MySmartPtr(cosnt MySmartPtr<T>& src) { mptr = new T(*src.mptr); }
不可以。因?yàn)榘凑章阒羔樀氖褂梅绞?,用戶本意是想將p1和p2都指向該int型堆區(qū)內(nèi)存,使用指針p1、p2都可改變該內(nèi)存空間的值,顯然深拷貝不符合此場景。
3.2 兩類智能指針
不帶引用計(jì)數(shù)的智能指針:只能有一個(gè)指針管理資源。
auto_ptr;
scoped_ptr;
unique_ptr;.
帶引用計(jì)數(shù)的智能指針:可以有多個(gè)指針同時(shí)管理資源。
shared_ptr;強(qiáng)智能指針。
weak_ptr: 弱智能指針。這是特例,不能控制資源的生命周期,不能控制資源的自動(dòng)釋放!
3.3 不帶引用計(jì)數(shù)的智能指針
只能有一個(gè)指針管理資源。
3.3.1 auto_ptr (不推薦使用)
void test03() { auto_ptr<int> ptr1(new int); auto_ptr<int> ptr2(ptr1); *ptr2 = 20; // cout << *ptr2 << endl; // 可訪問*ptr2 cout << *ptr1 << endl; //訪問*ptr1卻報(bào)錯(cuò) }
如上代碼,訪問*ptr1為何報(bào)錯(cuò)?
因?yàn)檎{(diào)用auto_ptr的拷貝構(gòu)造將ptr1的值賦值給ptr2后,底層會(huì)將ptr1指向nullptr;即將同一個(gè)指針拷貝構(gòu)造多次時(shí),只讓最后一次拷貝的指針管理資源,前面的指針全指向nullptr。
不推薦將auto_ptr存入容器。
3.3.2 scoped_ptr (使用較少)
scoped_ptr已將拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載delete了。
scoped_ptr(const scoped_ptr<T>&) = delete; // 刪除拷貝構(gòu)造 scoped_ptr<T>& operator=(const scoped_ptr<T>&) = delete; // 刪除賦值重載
3.3.3 unique_ptr (推薦使用)
unique_ptr也已將拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載delete。
unique_ptr(const unique_ptr<T>&) = delete; // 刪除拷貝構(gòu)造 unique_ptr<T>& operator=(const unique_ptr<T>&) = delete; // 刪除賦值重載
但unique_ptr提供了帶右值引用參數(shù)的拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載,如下:
void test04() { unique_ptr<int> ptr1(new int); // unique_ptr<int> ptr2(ptr1); 和scoped_ptr一樣無法通過編譯 unique_ptr<int> ptr2(std::move(ptr1)); // 但可使用move得到ptr1的右值類型 // *ptr1 也無法訪問 }
3.4 帶引用計(jì)數(shù)的智能指針
可以有多個(gè)指針同時(shí)管理資源。
原理:給智能指針添加其指向資源的引用計(jì)數(shù)屬性,若引用計(jì)數(shù) > 0,則不會(huì)釋放資源,若引用計(jì)數(shù) = 0就釋放資源。
具體來說:額外創(chuàng)建資源引用計(jì)數(shù)類,在智能指針類中加入該資源引用計(jì)數(shù)類的指針作為其中的一個(gè)屬性;當(dāng)使用裸指針創(chuàng)建智能指針對象時(shí),創(chuàng)建智能指針中的資源引用計(jì)數(shù)對象,并將其中的引用計(jì)數(shù)屬性初始化為1,當(dāng)后面對該智能指針對象進(jìn)行拷貝(使用其他智能指針指向該資源時(shí))或時(shí),需要在其他智能指針對象類中將被拷貝的智能指針對象中的資源引用計(jì)數(shù)類的指針獲取過來,然后將引用計(jì)數(shù)+1;當(dāng)用該智能指針給其他智能指針進(jìn)行賦值時(shí),因?yàn)槠渌悄苤羔槺毁x值后,它們就不指向原先的資源了,原先資源的引用計(jì)數(shù)就-1,直至引用計(jì)數(shù)為0時(shí)delete掉資源;當(dāng)智能指針對象析構(gòu)時(shí),會(huì)使用其中的資源引用計(jì)數(shù)指針將共享的引用計(jì)數(shù)-1,直至引用計(jì)數(shù)為0時(shí)delete掉資源。
shared_ptr:強(qiáng)智能指針;可改變資源的引用計(jì)數(shù)。
weak_ptr:弱智能指針;不可改變資源的引用計(jì)數(shù)。
帶引用計(jì)數(shù)的智能指針的簡單實(shí)現(xiàn):
/*資源的引用計(jì)數(shù)類*/ template<typename T> class RefCnt { public: RefCnt(T* ptr=nullptr):mptr(ptr) { if (mptr != nullptr) { mcount = 1; // 剛創(chuàng)建指針指針時(shí),引用計(jì)數(shù)初始化為1 } } void addRef() { // 增加引用計(jì)數(shù) mcount++; } int delRef() { // 減少引用計(jì)數(shù) mcount--; return mcount; } private: T* mptr; // 資源地址 int mcount; // 資源的引用計(jì)數(shù) }; /*智能指針類*/ template<typename T> class MySmartPtr { public: MySmartPtr(T* ptr = nullptr) :mptr(ptr) { // 創(chuàng)建該對象時(shí),裸指針會(huì)傳給對象 mpRefCnt = new RefCnt<T>(mptr); } ~MySmartPtr() { // 對象出作用域會(huì)自動(dòng)析構(gòu),因此會(huì)釋放裸指針指向的資源 if (0 == mpRefCnt->delRef()) { delete mptr; mptr = nullptr; } } // *運(yùn)算符重載 T& operator*() { // 提供智能指針的解引用操作,即返回它包裝的裸指針的解引用 return *mptr; } // ->運(yùn)算符重載 T* operator->() { // 即返回裸指針 return mptr; } // 拷貝構(gòu)造 MySmartPtr(const MySmartPtr<T>& src):mptr(src.mptr),mpRefCnt(src.mpRefCnt) { if (mptr != nullptr) { mpRefCnt->addRef(); } } // 賦值重載 MySmartPtr<T>& operator=(const MySmartPtr<T>& src) { if (this == &src) // 防止自賦值 return *this; /*若本指針改為指向src管理的資源,則本指針原先指向的資源的引用計(jì)數(shù)-1, 若原資源的引用計(jì)數(shù)為0,就釋放資源*/ if (0 == mpRefCnt->delRef()) { delete mptr; } mptr = src.mptr; mpRefCnt = src.mpRefCnt; mpRefCnt->addRef(); return *this; } private: T* mptr; // 指向資源的指針 RefCnt<T>* mpRefCnt; // 資源的引用計(jì)數(shù) };
強(qiáng)智能指針原理圖:
比如有如下創(chuàng)建強(qiáng)智能指針的語句:
shared_ptr<int> sp1(new int(10));
則如下所示:
(a)智能指針對象sp1中主要包括ptr指針指向其管理的資源,ref指針指向該資源的引用計(jì)數(shù),則顯然會(huì)開辟兩次內(nèi)存。
(b)uses為該資源的強(qiáng)智能指針的引用計(jì)數(shù),weaks為該資源的弱智能指針的引用計(jì)數(shù)。
3.4.1 shared_ptr
強(qiáng)智能指針??筛淖冑Y源的引用計(jì)數(shù)。
(1)強(qiáng)智能指針的交叉引用問題
class B; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } shared_ptr<B> _ptrb; }; class B { public: B() { cout << "B()" << endl; } ~B() { cout << "~B()" << endl; } shared_ptr<A> _ptra; }; void test06() { shared_ptr<A> pa(new A()); shared_ptr<B> pb(new B()); pa->_ptrb = pb; pb->_ptra = pa; /*打印pa、pb指向資源的引用計(jì)數(shù)*/ cout << pa.use_count() << endl; cout << pb.use_count() << endl; }
輸出結(jié)果:
可見pa、pb指向的資源的引用計(jì)數(shù)都為2,因此出了作用域?qū)е聀a、pb指向的資源都無法釋放,如下圖所示:
解決:
建議定義對象時(shí)使用強(qiáng)智能指針,引用對象時(shí)使用弱智能指針,防止出現(xiàn)交叉引用的問題。
什么是定義對象?什么是引用對象?
定義對象:
使用new創(chuàng)建對象,并創(chuàng)建一個(gè)新的智能指針管理它。
引用對象:
使用一個(gè)已存在的智能指針來創(chuàng)建一個(gè)新的智能指針。
定義對象和引用對象的示例如下:
shared_ptr<int> p1(new int()); // 定義智能指針對象p1 shared_ptr<int> p2 = make_shared<int>(10); // 定義智能指針對象p2 shared_ptr<int> p3 = p1; // 引用智能指針p1,并使用p3來共享它 weak_ptr<int> p4 = p2; // 引用智能指針p2,并使用p4來觀察它
如上述代碼,因?yàn)樵?strong>test06函數(shù)中使用pa對象的_ptrb引用pb對象,使用pb對象的_ptra引用pa對象,因此需要將A類、B類中的_ptrb和_ptra的類型改為弱智能指針weak_ptr即可,這樣就不會(huì)改變資源的引用計(jì)數(shù),能夠正確釋放資源。
3.4.2 weak_ptr
弱智能指針。不能改變資源的引用計(jì)數(shù)、不能管理對象生命周期、不能做到資源自動(dòng)釋放、不能創(chuàng)建對象,也不能訪問資源(因?yàn)閣eak_ptr未提供operator->和operator*運(yùn)算符重載),即不能通過弱智能指針調(diào)用函數(shù)、不能將其解引用。只能從一個(gè)已有的shared_ptr或weak_ptr獲得資源的弱引用。
弱智能指針weak_ptr若想用訪問資源,則需要使用lock方法將其提升為一個(gè)強(qiáng)智能指針,提升失敗則返回nullptr。(提升的情形常使用于多線程環(huán)境,避免無效的訪問,提升程序安全性)
注意:弱智能指針weak_ptr只能觀察資源的狀態(tài),但不能管理資源的生命周期,不會(huì)改變資源的引用計(jì)數(shù),不能控制資源的釋放。
weak_ptr示例:
void test07() { shared_ptr<Boy> boy_sptr(new Boy()); weak_ptr<Boy> boy_wptr(boy_sptr); // boy_wptr->study(); 錯(cuò)誤!無法使用弱智能指針訪問資源 cout << boy_sptr.use_count() << endl; // 引用計(jì)數(shù)為1,因?yàn)槿踔悄苤羔槻桓淖円糜?jì)數(shù) shared_ptr<int> i_sptr(new int(99)); weak_ptr<int> i_wptr(i_sptr); // cout << *i_wptr << endl; 錯(cuò)誤!無法使用弱智能指針訪問資源 cout << i_sptr.use_count() << endl; // 引用計(jì)數(shù)為1,因?yàn)槿踔悄苤羔槻桓淖円糜?jì)數(shù) /*弱智能指針提升為強(qiáng)智能指針*/ shared_ptr<Boy> boy_sptr1 = boy_wptr.lock(); if (boy_sptr1 != nullptr) { cout << boy_sptr1.use_count() << endl; // 提升成功,引用計(jì)數(shù)為2 boy_sptr1->study(); // 可以調(diào)用 } shared_ptr<int> i_sptr1 = i_wptr.lock(); if (i_sptr1 != nullptr) { cout << i_sptr1.use_count() << endl; // 提升成功,引用計(jì)數(shù)為2 cout << *i_sptr1 << endl; // 可以輸出 } }
4. 智能指針與多線程訪問共享資源的安全問題
現(xiàn)要實(shí)現(xiàn)主線程創(chuàng)建子線程,讓子線程執(zhí)行打印Hello的函數(shù),有如下兩種方式:
方式1:主線程調(diào)用test08函數(shù),在test08函數(shù)中啟動(dòng)子線程執(zhí)行線程函數(shù),如下:
void handler() { cout << "Hello" << endl; } void func() { thread t1(handler); } int main(int argc, char** argv) { func(); this_thread::sleep_for(chrono::seconds(1)); system("pause"); return 0; }
運(yùn)行報(bào)錯(cuò):
方式2:主線程中直接創(chuàng)建子線程來執(zhí)行線程函數(shù),如下:
void handler() { cout << "Hello" << endl; } int main(int argc, char** argv) { thread t1(handler); this_thread::sleep_for(chrono::seconds(1)); system("pause"); return 0; }
運(yùn)行結(jié)果:無報(bào)錯(cuò)
上面兩種方式都旨在通過子線程調(diào)用函數(shù)輸出Hello,但為什么方式1報(bào)錯(cuò)?很簡單,不再贅述。
回歸本節(jié)標(biāo)題的正題,有如下程序:
class C { public: C() { cout << "C()" << endl; } ~C() { cout << "~C()" << endl; } void funcC() { cout << "C::funcC()" << endl; } private: }; /*子線程執(zhí)行函數(shù)*/ void threadHandler(C* c) { this_thread::sleep_for(chrono::seconds(1)); c->funcC(); } /* 主線程 */ int main(int argc, char** argv) { C* c = new C(); thread t1(threadHandler, c); delete c; t1.join(); return 0; }
運(yùn)行結(jié)果:
結(jié)果顯示c指向的對象被析構(gòu)了,但是仍然使用該被析構(gòu)的對象調(diào)用了其中的funcC函數(shù),顯然不合理。
因此在線程函數(shù)中,使用c指針訪問A對象時(shí),需要觀察A對象是否存活。
使用弱智能指針weak_ptr接收對象,訪問對象之前嘗試提升為強(qiáng)智能指針shared_ptr,提升成功則訪問,否則對象被析構(gòu)。
情形1:對象被訪問之前就被析構(gòu)了:
class C { public: C() { cout << "C()" << endl; } ~C() { cout << "~C()" << endl; } void funcC() { cout << "C::funcC()" << endl; } private: }; /*子線程執(zhí)行函數(shù)*/ void threadHandler(weak_ptr<C> pw) { // 引用時(shí)使用弱智能指針 this_thread::sleep_for(chrono::seconds(1)); shared_ptr<C> ps = pw.lock(); // 嘗試提升 if (ps != nullptr) { ps->funcC(); } else { cout << "對象已經(jīng)析構(gòu)!" << endl; } } /* 主線程 */ int main(int argc, char** argv) { { shared_ptr<C> p(new C()); thread t1(threadHandler, weak_ptr<C>(p)); t1.detach(); } this_thread::sleep_for(chrono::seconds(5)); return 0; }
運(yùn)行結(jié)果:
情形2: 對象訪問完才被析構(gòu):
class C { public: C() { cout << "C()" << endl; } ~C() { cout << "~C()" << endl; } void funcC() { cout << "C::funcC()" << endl; } private: }; /*子線程執(zhí)行函數(shù)*/ void threadHandler(weak_ptr<C> pw) { // 引用時(shí)使用弱智能指針 this_thread::sleep_for(chrono::seconds(1)); shared_ptr<C> ps = pw.lock(); // 嘗試提升 if (ps != nullptr) { ps->funcC(); } else { cout << "對象已經(jīng)析構(gòu)!" << endl; } } /* 主線程 */ int main(int argc, char** argv) { { shared_ptr<C> p(new C()); thread t1(threadHandler, weak_ptr<C>(p)); t1.detach(); this_thread::sleep_for(chrono::seconds(5)); } return 0; }
運(yùn)行結(jié)果:
可見shared_ptr與weak_ptr結(jié)合使用,能夠較好地保證多線程訪問共享資源的安全。
5.智能指針的刪除器deleter
刪除器是智能指針釋放資源的方式,默認(rèn)使用操作符delete來釋放資源。
但并非所有智能指針管理的資源都可通過delete釋放,如數(shù)組、文件資源、數(shù)據(jù)庫連接資源等。
有如下智能指針對象管理一個(gè)數(shù)組資源:
unique_ptr<int> ptr1(new int[100]);
此時(shí)再用默認(rèn)的刪除器則會(huì)造成資源泄露,因此需要自定義刪除器。
一些為部分自定義刪除器的示例:
/* 方式1:類模板 */ template<typename T> class MyDeleter { public: void operator()(T* ptr) const { cout << "數(shù)組自定義刪除器1." << endl; delete[] ptr; } }; /* 方式2:函數(shù) */ void myDeleter(int* p) { cout << "數(shù)組自定義刪除器2." << endl; delete[] p; } void test09() { unique_ptr<int, MyDeleter<int>> ptr1(new int[100]); unique_ptr<int, void(*)(int*)> ptr2(new int[100], myDeleter); /* 方式3:Lambda表達(dá)式 */ unique_ptr<int, void(*)(int*)> ptr3(new int[100], [](int* p) { cout << "數(shù)組自定義刪除器3." << endl; delete[] p; }); } void test10() { unique_ptr<FILE, void(*)(FILE*)> ptr2(fopen("1.txt", "w"), [](FILE* f) { cout << "文件自定義刪除器." << endl; fclose(f); }); }
運(yùn)行結(jié)果:
6. make_shared和make_unique
6.1 make_shared
下面這種方式創(chuàng)建強(qiáng)智能指針存在缺陷。
shared_ptr<int> sp1(new int(10));
如下圖,假設(shè)為資源開辟內(nèi)存成功,但為引用計(jì)數(shù)結(jié)構(gòu)開辟內(nèi)存失敗,則shared_ptr創(chuàng)建sp1失敗,則不會(huì)去釋放new int(10)的資源,導(dǎo)致內(nèi)存泄漏。
因此,建議使用make_shared的方式創(chuàng)建強(qiáng)智能指針:
shared_ptr<int> sp1 = make_shared<int>(10); // 或者 auto sp1 = make_shared<int>(10); // 或者 auto sp1(make_shared<int>(10)); /*若有Test類,其中有兩個(gè)int型成員變量,則make_shared創(chuàng)建該類的智能指針的方式如下*/ shared_ptr<Test> sp2 = make_shared<Test>(1, 2); // 或者 auto sp2 = make_shared<Test>(1, 2); // 或者 auto sp2(make_shared<Test>(1, 2));
如下圖,make_shared創(chuàng)建智能指針時(shí),將資源的內(nèi)存和引用計(jì)數(shù)的內(nèi)存開辟在一起,因此只開辟一次內(nèi)存,要么開辟成功,要么開辟失敗,不存在像上面開辟兩次時(shí)可能導(dǎo)致內(nèi)存泄漏的問題。
因此make_shared的優(yōu)點(diǎn)有:
(a)內(nèi)存分配效率高;
(b)降低內(nèi)存泄漏風(fēng)險(xiǎn)。
但也存在缺點(diǎn):
(a)目前無法自定義刪除器;
(b)管理的內(nèi)存延遲釋放;
具體來說,原先的方式,無論弱引用計(jì)數(shù)weaks為多少,只要強(qiáng)引用計(jì)數(shù)uses為0,那塊int內(nèi)存就被釋放;注意:當(dāng)weaks和uses都為0時(shí),引用計(jì)數(shù)內(nèi)存才被釋放;
但由于make_shared的方式為資源內(nèi)存和引用計(jì)數(shù)開辟一整塊內(nèi)存,只要weaks和uses有不為0的,這一整塊內(nèi)存就不會(huì)被釋放。
6.2 make_unique
同樣的,建議使用make_unique。
優(yōu)點(diǎn):
(a)內(nèi)存分配效率高;
(b)降低內(nèi)存泄漏風(fēng)險(xiǎn)。
缺點(diǎn):
(a)目前無法自定義刪除器;
以上就是一文詳解C++ 智能指針的原理、分類及使用的詳細(xì)內(nèi)容,更多關(guān)于C++ 智能指針的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
pcl1.8.0+vs2013環(huán)境配置超詳細(xì)教程
這篇文章主要介紹了pcl1.8.0+vs2013環(huán)境配置超詳細(xì)教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07C語言常用標(biāo)準(zhǔn)頭文件使用實(shí)例
在C的系列語言程序中,頭文件(通常擴(kuò)展名為.h)被大量使用,它通常包含函數(shù)、變量、結(jié)構(gòu)體等的聲明和定義,以及一些宏定義和類型定義,這篇文章主要給大家介紹了關(guān)于C語言常用標(biāo)準(zhǔn)頭文件使用的相關(guān)資料,需要的朋友可以參考下2024-08-08C語言 詳細(xì)解析時(shí)間復(fù)雜度與空間復(fù)雜度
算法復(fù)雜度分為時(shí)間復(fù)雜度和空間復(fù)雜度。其作用: 時(shí)間復(fù)雜度是度量算法執(zhí)行的時(shí)間長短;而空間復(fù)雜度是度量算法所需存儲(chǔ)空間的大小2022-04-04C語言實(shí)現(xiàn)學(xué)生管理系統(tǒng)總結(jié)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)學(xué)生管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07全面了解#pragma once與 #ifndef的區(qū)別
下面小編就為大家?guī)硪黄媪私?pragma once與 #ifndef的區(qū)別。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-08-08C++ Log日志類輕量級(jí)支持格式化輸出變量實(shí)現(xiàn)代碼
這篇文章主要介紹了C++ Log日志類輕量級(jí)支持格式化輸出變量實(shí)現(xiàn)代碼,需要的朋友可以參考下2019-04-04