咱們平時使用C++開發過程當中或多或少都會使用std::string,但您瞭解string具體是如何實現的嗎,這裏程序喵給你們從源碼角度分析一下。javascript
讀完本文相信您能夠回答如下問題:
php
▼ string的常見的實現方式有幾種?
html
▼ string類的內部結構是什麼樣子?java
▼ string內部使用的內存是如何分配管理的?c++
▼ string是如何拷貝構造,如何析構的,有引用計數的概念嗎?
web
▼ string的data()和c_str()函數有什麼區別?
微信
▼ std::to_string()是如何實現的?
markdown
常見的string實現方式有兩種,一種是深拷貝的方式,一種是COW(copy on write)寫時拷貝方式,之前多數使用COW方式,但因爲目前多線程使用愈來愈多,COW技術在多線程中會有額外的性能惡化,因此如今多數使用深拷貝的方式,但瞭解COW的技術實現仍是頗有必要的。
多線程
這裏會對這兩種方式都進行源碼分析,正文內容較少,更多內容都在源碼的註釋中。
ide
string的內容主要在gcc源碼的三個文件中:<string>、<basic_string.h>、<basic_string.tcc>
在分析前先介紹下string或者C++ stl中幾個基本的概念:
size: 表示真實數據的大小,通常resize函數改變的就是這個值。
capacity:表示內部實際已經分配的內存大小,capacity必定大於等於size,當size超過這個容量時會觸發從新分配機制,通常reserve函數改變的就是這個值。
深拷貝下string的實現
<string>文件中有以下代碼:
// file: stringusing string = basic_string<char>;
這裏能夠看到string其實真實的樣子是basic_string,這裏能夠看下basic_string真實的結構:
template <typename _CharT, typename _Traits, typename _Alloc>class basic_string { // Use empty-base optimization: http://www.cantrip.org/emptyopt.html struct _Alloc_hider : allocator_type // TODO check __is_final { _Alloc_hider(pointer __dat, const _Alloc& __a) : allocator_type(__a), _M_p(__dat) {}
_Alloc_hider(pointer __dat, _Alloc&& __a = _Alloc()) : allocator_type(std::move(__a)), _M_p(__dat) {}
/** * _M_p指向實際的數據 */ pointer _M_p; // The actual data. };
_Alloc_hider _M_dataplus; /** * 真實數據的長度,等價於前面介紹的STL中的size */ size_type _M_string_length;
enum { _S_local_capacity = 15 / sizeof(_CharT) };
/** * 這裏有個小技巧,用了union * 由於使用_M_local_buf時候不須要關注_M_allocated_capacity * 使用_M_allocated_capacity時就不須要關注_M_local_buf * 繼續向下看完您就會明白。 */ union { _CharT _M_local_buf[_S_local_capacity + 1]; /** * 內部已經分配的內存的大小,等價於前面介紹的STL中的capacity */ size_type _M_allocated_capacity; };};
從這裏能夠看見整個basic_string的結構如圖:
看下面代碼:
string str;
這段代碼會調用普通構造函數,對應的源碼實現以下:
basic_string() : _M_dataplus(_M_local_data()) { _M_set_length(0); }
而_M_local_data()的實現以下:
const_pointer _M_local_data() const { return std::pointer_traits<const_pointer>::pointer_to(*_M_local_buf);}
這裏能夠看見M_dataplus表示實際存放數據的地方,當string是空的時候,其實就是指向M_local_buf,且_M_string_length是0。
當由char*構造string時,構造函數以下:
basic_string(const _CharT* __s, size_type __n, const _Alloc& __a = _Alloc()) : _M_dataplus(_M_local_data(), __a) { _M_construct(__s, __s + __n);}
首先讓M_dataplus指向local_buf,再看下M_construct的實現,具體分析能夠看下我代碼中添加的註釋:
/**** _M_construct有不少種不一樣的實現,不一樣的迭代器類型有不一樣的優化實現,* 這裏咱們只須要關注一種便可,總體思路是相同的。*/template <typename _CharT, typename _Traits, typename _Alloc>template <typename _InIterator>void basic_string<_CharT, _Traits, _Alloc>::_M_construct(_InIterator __beg, _InIterator __end, std::input_iterator_tag) { size_type __len = 0; size_type __capacity = size_type(_S_local_capacity); // 如今__capacity是15,注意這個值等會可能會改變
while (__beg != __end && __len < __capacity) { _M_data()[__len++] = *__beg; ++__beg; } /** 如今_M_data()指向的是_M_local_buf * 上面最多會拷貝_S_local_capacity即15個字節,繼續往下看, * 當超過_S_local_capacity時會從新申請一塊堆內存,_M_data()會去指向這塊新內存 */ __try { while (__beg != __end) { if (__len == __capacity) { /** * 就是在這裏,當string內capacity不夠容納len個字符時,會使用_M_create去擴容 * 這裏你可能會有疑惑,貌似每次while循環都會去從新使用_M_create來申請多一個字節的內存 * 但其實不是,_M_create的第一個參數的傳遞方式是引用傳遞,__capacity會在內部被修改,稍後會分析 */ __capacity = __len + 1; pointer __another = _M_create(__capacity, __len); /** * 把舊數據拷貝到新的內存區域,_M_data()指向的是舊數據,__another指向的是新申請的內存 */ this->_S_copy(__another, _M_data(), __len); /** * __M_dispose() * 釋放_M_data()指向的舊數據內存,若是是_M_local_buf則不須要釋放,稍後分析 */ _M_dispose(); /** * _M_data() * 內部的指向內存的指針指向這塊新申請的內存__another,它的實現其實就是 * void _M_data(pointer __p) { _M_dataplus._M_p = __p; } */ _M_data(__another); /** * _M_allocated_capacity設置爲__capacity * 實現爲 void _M_capacity(size_type __capacity) { _M_allocated_capacity = __capacity; } */ _M_capacity(__capacity); } _M_data()[__len++] = *__beg; ++__beg; } } __catch(...) { /** * 異常發生時,避免內存泄漏,會釋放掉內部申請的內存 */ _M_dispose(); __throw_exception_again; } /** * 最後設置string的長度爲__len * 實現爲void _M_length(size_type __length) { _M_string_length = __length; } */ _M_set_length(__len);}
再分析下內部的內存申請函數_M_create:
/*** @brief _M_create表示申請新內存* @param __capacity 想要申請的內存大小,注意這裏參數傳遞方式是引用傳遞,內部會改變其值* @param __old_capacity 之前的內存大小*/template <typename _CharT, typename _Traits, typename _Alloc>typename basic_string<_CharT, _Traits, _Alloc>::pointer basic_string<_CharT, _Traits, _Alloc>::_M_create( size_type& __capacity, size_type __old_capacity) { /** * max_size()表示標準庫容器規定的一次性能夠分配到最大內存大小 * 當想要申請的內存大小最大規定長度時,會拋出異常 */ if (__capacity > max_size()) std::__throw_length_error(__N("basic_string::_M_create"));
/** * 這裏就是常見的STL動態擴容機制,其實常見的就是申請爲__old_capacity的2倍大小的內存,最大隻能申請max_size() * 註釋只是說了常見的內存分配大小思想,不全是下面代碼的意思,具體能夠直接看下面這幾行代碼哈 */ if (__capacity > __old_capacity && __capacity < 2 * __old_capacity) { __capacity = 2 * __old_capacity; // Never allocate a string bigger than max_size. if (__capacity > max_size()) __capacity = max_size(); }
/** * 使用內存分配子去分配__capacity+1大小的內存,+1是爲了多存儲個\0 */ return _Alloc_traits::allocate(_M_get_allocator(), __capacity + 1);}
再分析下內部的內存釋放函數_M_dispose函數:
/*** 若是當前指向的是本地內存那15個字節,則不須要釋放* 若是不是,則須要使用_M_destroy去釋放其指向的內存*/void _M_dispose() { if (!_M_is_local()) _M_destroy(_M_allocated_capacity);}
/*** 判斷下當前內部指向的是否是本地內存* _M_local_data()即返回_M_local_buf的地址*/bool _M_is_local() const { return _M_data() == _M_local_data(); }
void _M_destroy(size_type __size) throw() { _Alloc_traits::deallocate(_M_get_allocator(), _M_data(), __size + 1);}
再分析下basic_string的拷貝構造函數:
/*** basic_string的拷貝構造函數* 其實就是每次都作一次深拷貝*/basic_string(const basic_string& __str) : _M_dataplus(_M_local_data(), _Alloc_traits::_S_select_on_copy(__str._M_get_allocator())) { _M_construct(__str._M_data(), __str._M_data() + __str.length());}
再分析下basic_string的賦值構造函數:
/*** 賦值構造函數,調用了assign函數*/basic_string& operator=(const basic_string& __str) { return this->assign(__str); }
/*** 調用了_M_assign函數*/basic_string& assign(const basic_string& __str) { this->_M_assign(__str); return *this;}
/*** 賦值的核心函數*/template <typename _CharT, typename _Traits, typename _Alloc>void basic_string<_CharT, _Traits, _Alloc>::_M_assign(const basic_string& __str) { if (this != &__str) { const size_type __rsize = __str.length(); const size_type __capacity = capacity();
/** * 若是capacity不夠用,須要進行從新分配 */ if (__rsize > __capacity) { size_type __new_capacity = __rsize; pointer __tmp = _M_create(__new_capacity, __capacity); _M_dispose(); _M_data(__tmp); _M_capacity(__new_capacity); }
/** * 將__str指向的內存拷貝到當前對象指向的內存上 */ if (__rsize) this->_S_copy(_M_data(), __str._M_data(), __rsize);
_M_set_length(__rsize); }}
再分析下移動構造函數:
/*** 移動構造函數,其實就是把src指向的內存移動到了dst種*/basic_string(basic_string&& __str) noexcept : _M_dataplus(_M_local_data(), std::move(__str._M_get_allocator())) { if (__str._M_is_local()) { traits_type::copy(_M_local_buf, __str._M_local_buf, _S_local_capacity + 1); } else { _M_data(__str._M_data()); _M_capacity(__str._M_allocated_capacity); }
// Must use _M_length() here not _M_set_length() because // basic_stringbuf relies on writing into unallocated capacity so // we mess up the contents if we put a '\0' in the string. _M_length(__str.length()); __str._M_data(__str._M_local_data()); __str._M_set_length(0);}
移動賦值函數和移動構造函數相似,就不做過多分析啦。
COW方式下string的實現
先看下部分源代碼瞭解下COW的basic_string的結構:
template <typename _CharT, typename _Traits, typename _Alloc>class basic_string { private: struct _Rep_base { /** * string實際數據的大小 * 字符串真正存儲的是正常字符串數據加上一個\0,真正的長度實際上是_M_length+1 */ size_type _M_length; /** * string當前已經分配了的內存大小 * _M_capacity必定不小於_M_length,內存分配老是以_M_capacity+1爲單位 */ size_type _M_capacity; /** * _M_refcount表示string的引用計數,取值能夠分爲三種: * -1:可能內存泄漏,有一個變量指向字符串,字符串能夠被更改,不容許拷貝,當 * _M_refcount爲-1時,表示這個string對象不會再和其它string對象共享啦。 * 0:有一個變量指向字符串,字符串能夠被更改。 * n>=1:有n+1個變量指向字符串,對該字符串操做時應該加鎖,字符串不能夠被更改。 */ _Atomic_word _M_refcount; };
/** * _Rep繼承自_Rep_base * 主要目的就是繼承_Rep_base的三個成員_M_length、_M_capacity、_M_refcount */ struct _Rep : _Rep_base { // Types: typedef typename _Alloc::template rebind<char>::other _Raw_bytes_alloc;
static const size_type _S_max_size; static const _CharT _S_terminal; // \0
static size_type _S_empty_rep_storage[]; // 這裏大小不是0,稍後分析
static _Rep& _S_empty_rep() _GLIBCXX_NOEXCEPT { // NB: Mild hack to avoid strict-aliasing warnings. Note that // _S_empty_rep_storage is never modified and the punning should // be reasonably safe in this case. void* __p = reinterpret_cast<void*>(&_S_empty_rep_storage); return *reinterpret_cast<_Rep*>(__p); } };
// Use empty-base optimization: http://www.cantrip.org/emptyopt.html struct _Alloc_hider : _Alloc { _Alloc_hider(_CharT* __dat, const _Alloc& __a) _GLIBCXX_NOEXCEPT : _Alloc(__a), _M_p(__dat) {}
_CharT* _M_p; // The actual data,這裏的_M_p指向存儲實際數據的對象地址 };
public: static const size_type npos = static_cast<size_type>(-1); // 0xFFFFFFFF
private: /** * _M_dataplus是basic_string內部惟一的一個成員變量, * 內部有個_M_p成員,指向存儲實際的數據的對象,是_Rep對象的指針 */ mutable _Alloc_hider _M_dataplus;};
具體分析能夠看代碼中註釋,能夠分析出COW的string結構如圖:
前面程序喵分析過深拷貝方式下string的局部內存爲M_local_buf,那COW下string的S_empty_rep_storage是什麼樣子呢?直接看源代碼:
// Linker sets _S_empty_rep_storage to all 0s (one reference, empty string)// at static init time (before static ctors are run).template <typename _CharT, typename _Traits, typename _Alloc>typename basic_string<_CharT, _Traits, _Alloc>::size_type basic_string<_CharT, _Traits, _Alloc>::_Rep:: _S_empty_rep_storage[(sizeof(_Rep_base) + sizeof(_CharT) + sizeof(size_type) - 1) / sizeof(size_type)];
再分析下構造函數:
/*** 使_M_dataplus指向_S_construct函數返回的內存*/template <typename _CharT, typename _Traits, typename _Alloc>basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT* __s, size_type __n, const _Alloc& __a) : _M_dataplus(_S_construct(__s, __s + __n, __a), __a) {}
/*** 返回一段內存,這段內存能夠是本地空字符的內存,也能夠是內存分配單元分配的內存*/template <typename _CharT, typename _Traits, typename _Alloc>template <typename _InIterator>_CharT* basic_string<_CharT, _Traits, _Alloc>::_S_construct(_InIterator __beg, _InIterator __end, const _Alloc& __a, input_iterator_tag) { if (__beg == __end && __a == _Alloc()) return _S_empty_rep()._M_refdata(); // Avoid reallocation for common case. _CharT __buf[128]; size_type __len = 0; while (__beg != __end && __len < sizeof(__buf) / sizeof(_CharT)) { __buf[__len++] = *__beg; ++__beg; } /** * len < 128字節時,分配len字節 * 不然,以128爲單位,每次擴容2倍大小 * 稍後相信分析 */ _Rep* __r = _Rep::_S_create(__len, size_type(0), __a); /** * 將__buf指向的內存拷貝到數據真實存放的地址,_M_refdata()指向數據真實存放的地址 * _M_refdata()函數實現以下,能夠經過上面畫的string結構圖分析: * _CharT* _M_refdata() throw() { return reinterpret_cast<_CharT*>(this + 1); } * this+1就是數據真正的地址,這裏的1表明sizeof(_Rep) */ _M_copy(__r->_M_refdata(), __buf, __len); __try { /** * 這裏的擴容機制和上面介紹的相同,這裏就不過多介紹 */ while (__beg != __end) { if (__len == __r->_M_capacity) { // Allocate more space. _Rep* __another = _Rep::_S_create(__len + 1, __len, __a); _M_copy(__another->_M_refdata(), __r->_M_refdata(), __len); __r->_M_destroy(__a); __r = __another; } __r->_M_refdata()[__len++] = *__beg; ++__beg; } } __catch(...) { __r->_M_destroy(__a); __throw_exception_again; } /** * 設置string的長度,同時設置該string是可共享的,稍後分析 */ __r->_M_set_length_and_sharable(__len); return __r->_M_refdata();}
再看下string內部_M_create是如何申請內存的
template <typename _CharT, typename _Traits, typename _Alloc>typename basic_string<_CharT, _Traits, _Alloc>::_Rep* basic_string<_CharT, _Traits, _Alloc>::_Rep::_S_create( size_type __capacity, size_type __old_capacity, const _Alloc& __alloc) { if (__capacity > _S_max_size) __throw_length_error(__N("basic_string::_S_create"));
/** * __pagesize是頁的大小,每次內存分配的最小單位 * __malloc_header_zize是malloc分配內存額外須要的空間,存儲內存實際的長度信息 */ const size_type __pagesize = 4096; const size_type __malloc_header_size = 4 * sizeof(void*);
/** * 每次兩倍擴容 */ if (__capacity > __old_capacity && __capacity < 2 * __old_capacity) __capacity = 2 * __old_capacity;
/** * 看了前面的結構圖您應該就能明白爲何是這麼計算,這裏的+1是存儲字符串的結束符 */ size_type __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep);
/** * 由於內存是以頁爲基本單位分配的,因此這裏作了一些優化,保證分配內存的大小是內存頁的整數倍 */ const size_type __adj_size = __size + __malloc_header_size; if (__adj_size > __pagesize && __capacity > __old_capacity) { const size_type __extra = __pagesize - __adj_size % __pagesize; __capacity += __extra / sizeof(_CharT); // Never allocate a string bigger than _S_max_size. if (__capacity > _S_max_size) __capacity = _S_max_size; __size = (__capacity + 1) * sizeof(_CharT) + sizeof(_Rep); }
// NB: Might throw, but no worries about a leak, mate: _Rep() // does not throw. void* __place = _Raw_bytes_alloc(__alloc).allocate(__size); /** * 這裏是placement new,表示在__place內存位置處調用_Rep構造函數 */ _Rep* __p = new (__place) _Rep; __p->_M_capacity = __capacity; /** * 設置其可共享,實現以下 * void _M_set_sharable() _GLIBCXX_NOEXCEPT { this->_M_refcount = 0; } */ __p->_M_set_sharable(); return __p;}
這裏有關於malloc的知識點能夠看我以前寫的文章:xxx前面Rep有個_M_set_length_and_sharable方法,看下它的源碼:
/** * 若是當前內存指向地址是本地內存則什麼都不作,不然 * 設置長度爲n * 設置其可共享,其實就是設置引用計數爲0 * 同時在最後添加一個結束符\0 */void _M_set_length_and_sharable(size_type __n) _GLIBCXX_NOEXCEPT { if (__builtin_expect(this != &_S_empty_rep(), false)) { this->_M_set_sharable(); // One reference. this->_M_length = __n; traits_type::assign(this->_M_refdata()[__n], _S_terminal); }}
void _M_set_sharable() _GLIBCXX_NOEXCEPT { this->_M_refcount = 0; }
COW版本主要就是爲了不過多的拷貝,這裏看下string的拷貝構造函數:
/** * 這裏是string的構造函數,主要是調用_Rep的_M_grab函數 */basic_string(const basic_string& __str, const _Alloc& __a) : _M_dataplus(__str._M_rep()->_M_grab(__a, __str.get_allocator()), __a) {}
/** * 前面已經介紹過爲何+1,這裏您應該就知道爲何-1啦 */_Rep* _M_rep() const _GLIBCXX_NOEXCEPT { return &((reinterpret_cast<_Rep*>(_M_data()))[-1]); }
/** * _M_grab函數決定是將引用計數+1仍是拷貝一份 * 若是_M_is_leaked()表示不能夠共享,則須要拷貝一份 */_CharT* _M_grab(const _Alloc& __alloc1, const _Alloc& __alloc2) { return (!_M_is_leaked() && __alloc1 == __alloc2) ? _M_refcopy() : _M_clone(__alloc1);}
/** * 若是引用計數小於0,則爲true,前面有過約定 */bool _M_is_leaked() const _GLIBCXX_NOEXCEPT { // _M_refcount is mutated concurrently by _M_refcopy/_M_dispose, // so we need to use an atomic load. However, _M_is_leaked // predicate does not change concurrently (i.e. the string is either // leaked or not), so a relaxed load is enough. return __atomic_load_n(&this->_M_refcount, __ATOMIC_RELAXED) < 0; return this->_M_refcount < 0;}
/** * 引用拷貝,其實就是引用計數+1 */_CharT* _M_refcopy() throw() {#if _GLIBCXX_FULLY_DYNAMIC_STRING == 0 if (__builtin_expect(this != &_S_empty_rep(), false))#endif __gnu_cxx::__atomic_add_dispatch(&this->_M_refcount, 1); return _M_refdata();} // XXX MT
/** * 深拷貝 */template <typename _CharT, typename _Traits, typename _Alloc>_CharT* basic_string<_CharT, _Traits, _Alloc>::_Rep::_M_clone(const _Alloc& __alloc, size_type __res) { // Requested capacity of the clone. const size_type __requested_cap = this->_M_length + __res; _Rep* __r = _Rep::_S_create(__requested_cap, this->_M_capacity, __alloc); if (this->_M_length) _M_copy(__r->_M_refdata(), _M_refdata(), this->_M_length);
__r->_M_set_length_and_sharable(this->_M_length); return __r->_M_refdata();}
再分析下string的析構函數:
/** * string的析構函數,調用了_M_dispose函數 */~basic_string() _GLIBCXX_NOEXCEPT { _M_rep()->_M_dispose(this->get_allocator()); }
/** * 將引用計數-1,若是引用計數 <= 0,則釋放內存 */void _M_dispose(const _Alloc& __a) _GLIBCXX_NOEXCEPT { if (__builtin_expect(this != &_S_empty_rep(), false)) { // Be race-detector-friendly. For more info see bits/c++config. _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&this->_M_refcount); if (__gnu_cxx::__exchange_and_add_dispatch(&this->_M_refcount, -1) <= 0) { _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&this->_M_refcount); _M_destroy(__a); } }} // XXX MT
template <typename _CharT, typename _Traits, typename _Alloc>void basic_string<_CharT, _Traits, _Alloc>::_Rep::_M_destroy(const _Alloc& __a) throw() { const size_type __size = sizeof(_Rep_base) + (this->_M_capacity + 1) * sizeof(_CharT); _Raw_bytes_alloc(__a).deallocate(reinterpret_cast<char*>(this), __size);}
data()和c_str()的區別
咱們之前學習工做過程當中都知道str有data和c_str函數,看資料都說它們的區別是一個帶\0結束符,一個不帶。這裏看下源碼:
const _CharT* c_str() const _GLIBCXX_NOEXCEPT { return _M_data(); }
const _CharT* data() const _GLIBCXX_NOEXCEPT { return _M_data(); }
這裏能夠看見它倆沒有任何區別,由於\0結束符其實在最開始構造string對象的時候就已經添加啦。
to_string是怎麼實現的?
這裏直接看代碼:
inline string to_string(int __val) { return __gnu_cxx::__to_xstring<string>(&std::vsnprintf, 4 * sizeof(int), "%d", __val);}
inline string to_string(unsigned __val) { return __gnu_cxx::__to_xstring<string>(&std::vsnprintf, 4 * sizeof(unsigned), "%u", __val);}
inline string to_string(long __val) { return __gnu_cxx::__to_xstring<string>(&std::vsnprintf, 4 * sizeof(long), "%ld", __val);}
template <typename _String, typename _CharT = typename _String::value_type>_String __to_xstring(int (*__convf)(_CharT*, std::size_t, const _CharT*, __builtin_va_list), std::size_t __n, const _CharT* __fmt, ...) { // XXX Eventually the result should be constructed in-place in // the __cxx11 string, likely with the help of internal hooks. _CharT* __s = static_cast<_CharT*>(__builtin_alloca(sizeof(_CharT) * __n));
__builtin_va_list __args; __builtin_va_start(__args, __fmt);
const int __len = __convf(__s, __n, __fmt, __args);
__builtin_va_end(__args);
return _String(__s, __s + __len);}
這裏能夠看出全部的數值類型轉string,都是經過vsnprintf來實現,具體vsnprintf是什麼這裏就不過多介紹啦,讀者能夠自行查找下相關用法哈。
關於std::string的分析就到這裏,前面爲了讓您看源碼看的更清晰,程序喵對代碼添加了詳細的註釋,同時作了適當的刪減,但必定是正確的源代碼,你們可放心閱讀。相信您看完上面的源碼分析能夠回答出文章開頭那幾個問題並有所收穫。
參考資料
gcc-9.2.0源代碼 <string> 文件
gcc-9.2.0源代碼<basic_string.h>文件
gcc-9.2.0源代碼<basic_string.tcc>文件
https://blog.csdn.net/sdoyuxuan/article/details/76230520
https://blog.csdn.net/ybxuwei/article/details/51326830
https://my.oschina.net/u/3882675/blog/1925372
往期推薦
本文分享自微信公衆號 - 程序喵大人(dmjjzl1)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。