C++自修入門實境秀、C++ Primer 5版研讀秀 87/~12.3 Using the Library A Text-Query Prog...



頁476

12.2. Dynamic Arrays

動態陣列(其實是指標,指向元素型別)

第87集8:29:00 動態陣列其實就是動態配置的多個物件(動態配置物件(dynamically allocated object)的複數)





頁479

for(size_t i=0;i!=10;++i)

up[i]=i;//因為動態陣列不是真的陣列,只是一個指向元素型別的指標,由此可以直接用up這樣的智慧指標來作下標的動作。非也!乃unique_ptr在對動態陣列時定義了它自己的下標運算子([]運算子,subscript operator)運算,故不需再解參考。解參考運算子似乎指向動態陣列的unique_ptr也不支援。Table 12.6. unique_ptrs to Arrays 表12.6 指向動態陣列的unique_ptr特有的運算第87集一開始 0:25 9:00

將up所指陣列的每個元素,指定為i此變數之值。





頁480表12.6

u[i] 第87集開始 會回傳u指向的陣列在i索引值位置上的元素值(物件)。u必須指向動態陣列(多個物件),不能指向單一物件







頁481

第87集10:50 15:33 17:40 30:30

12.2.2. The allocator Class

配置器類別(分配器類別)

34:10 2:5:55

new運算子的運算之所以會有它的局限性(limits its flexibility),就是在於它是將兩個工作結合在一塊的緣故:一個是對所需記憶體的配置,另一個是在所配置出來的記憶體區塊中,進行的物件建構。同樣地,delete運算子也將解構(destruction)與釋放(清除deallocation)的工作結合在一塊了。若是要配置的是單一物件的話,那麼能將初始化(建構)物件和配置記憶體資源這兩項工作結合在一塊做,2:12:40確實能達到我們所想要的結果。(這確實是我們想要的,不會浪費記憶體資源)43:00在我們大概都能預先明白(certainly)知道(should)要配置出來的單一物件應(該、會)有什麼值,在這種情形下,結合建構與配置的操作才是很實際的。

結合「建構」與「配置」的操作可合稱為「建置」

47:20 block關鍵字出現了!(不見題目只見關鍵字)

然而當配置(allocate)一個記憶體區塊(記憶體區域,block of memory)後,我們通常都會在需要的時候才會在那個區塊去建構(construct)我們所需要用到的物件。若是這種情況,先配置、後建構(初始化)的概念,我們就會想要去試著把配置記憶體資源與物件建構的工作給分割開來。而將建構物件的時機從配置記憶體的同時獨立出來,就意謂著我們知道我們有足夠的資源可以在所配置出來的記憶體中,作大範圍的切割,並且只會在我們真的需要創建(create建構)物件時才會耗用建構對象所需用到的資源。之所以會想要將建構與配置分割開來做,是因為通常將配置與建構工作結合起來會是非常耗用資源的,舉例來說:59:25 2:21:10

string *const p = new string[n]; //建構n個空的string字串(預設初始化)

string s;

string *q = p; //q指向了p動態陣列中的第1個字串元素

while (cin >> s && q != p + n) //「cin>>s」:讀取標準輸入到s

*q++ = s; //將一個新值(=s)指定給q指向的元素

const size_t size = q - p; //記下已經讀取了多少個動態陣列p中的字串元素

//……

//對這個動態陣列的使用

delete[] p; //因為普通常值(恆定)指標(內建型別為對string的指標)p指向的是一個動態陣列,所以對它delete必須加上[]空的方括號

1:7:08 2:31:10在new述句中配置並初始化(建構,其實是預設初始化)了 n 個string,然而這些n個string 也許不會全都用到;可能只需要其中的一部分就好。因此(於是),在這種情況下,我們可能就會製造出太多的垃圾(caeated objcets that are never used),甚至,即使是對於那些我們會用到的字串來說,我們也可能沒多久就指定一個新的值給它,而逕捨棄了先前已經初始化的字串值。因為一旦其值經過指定,先前初始化它們所配置的資源便無用了。也就是說,對於有用上的元素物件,實際已然經過兩次的覆寫:第一次是當它們被預設初始化時,另一次則是在指定新值給它們的時候。1:20:50 2:34:50

更有甚者,那些沒有定義自己預設建構器(即無參數的建構器constructor)的類別是無法被動態配置成一個陣列的。

More importantly, classes that do not have default constructors cannot be dynamically allocated as an array.

是不是因為有預設建構器的類別可以用類別的預設初始化來初始化其型別物件,而無者則需再「指定」初始器來初始化其型別物件之值?

1:26:10 2:39:40

The allocator Class

配置器類別

C++的程式庫在memory標頭檔中定義了一個叫做allocator的類別,這種類別足以擔任配置與建構分離的工作。它會配置出一個原初(原生、生的raw)的已知型別而未經物件建構的記憶體區塊

It provides type-aware allocation of raw, unconstructed,memory.

下頁的表12.7列出了allocator類別支援的運算/操作。在現在我們這一區段課文中,我們會先討論allocator的運算,在§13.5(頁525)則會看到這個allocator類別實際運作的情形是怎樣的(In § 13.5 (p. 524), we’ll see an example of how this class is typically used.)。

就像vector,allocator類別也是一個模板類別(template,§3.3,頁96)。要定義一個allocator的類別物件(型別物件)就必須指定想要讓這個allocator去配置(allocate)的物件其型別是什麼。當allocator型別物件在配置記憶體時,它會分配適當大小且整齊並排的(aligned)記憶體給要儲存在其中的特定型別物件使用:

When an allocator object allocates memory, it allocates memory that is appropriately sized and aligned to hold objects of the given type:

allocator<string> alloc;//宣告、定義一個allocator型別物件,它可以配置出一個存放string型別的記憶體資源(區塊)

object that can allocate strings

auto const p = alloc.allocate(n); //用alloc這個allocator型別物件來配置n個未經建構(unconstructed應即是未經初始化)的字串。

像這樣對allocate成員函式的呼叫就會為n個string物件配置記憶體資源(記憶體區塊)。

改中文版作文

12.2.2 allocator 類別

new本身有的一個面向機制限制了其它的彈性,就是:new結合了記憶體的配置,、以及在那個記憶體中建構物件,這兩個的動作。同樣地,delete也結合了解構(destruction)和釋放(deallocation)這兩個動作。在配置單一個物件時,若結合了配置和初始化(就是建構)通常就是我們想要的。在這種情況中,我們幾乎能夠確定該物件應該要有什麼值。

當我們配置一個區塊的記憶體,我們經常通常會計畫打算在必要時,才於在那個記憶體中建構出我們所要用到的物件。在有這種情況需求中時,我們就會想要將記憶體的配置和物件的建構區分開來。將建構與配置分離,就意味著我們有足夠的可以配置大量的記憶體資本(資源成本)來供我們差遣(遣用),並且只在實際需要派上用場建立它們時,再才給付出建構物件所需要用到的額外負擔成本。

一般來說來,將配置和建構結合在一起,可能常會浪費不必要的資本(資源成本)。舉例來說:

string *const p = new string[n]; //建構 n 個空的 string

string s;

string *q = p; // q指向第一個 string

while (cin >> s && q != p + n)

*q++ = s; //指定一個新的值給*q

const size_t size = q - p; //記住我們讀取了多少個 string

//使用此陣列

delete[] p; // p指向一個陣列;必須記得使用delete[]

這個new運算式配置並初始化了 n個string。然而,我們可能不需要用到n個string,或許少一點可能就夠用了。因此,我們可能會建立出從未被使用過的物件。此外,對於我們確實有用到的物件,我們可能也會不久就即刻指定一個新的值來蓋過它之前初始化的string值,所以,即使有所用上的元素,在實際上也會被寫入了兩次:第一次是在預設初始化時,以及第二次則是後續後續對它們指定值時。

更重要的是,沒有預設建構器的類別無法動態配置為一個陣列。

allocator類別

3:15:55 程式庫的allocator類別,定義於memory標頭中,能讓我們分離將配置與建構的工作分開來進行。它可配置具有型別的、未經建構的原始記憶體。表12.7描述了 allocator支援的運算。在本節中,我們會介紹allocator的運算。§13.5中(頁525),我們會看到通常如何使用例子展示這個類別通常會如何被使用的例子。

就像vector,allocator是一種模板(§3.3)。要定義一個allocator,我們必須指定一個那個特定的allocator能夠配置的物件型別。一個allocator物件配置記憶體時,它所配置出來的記憶體是有適當的大小的,並經過對齊排列的,能夠以便存放給指定型別的物件:

allocator<string> alloc; //能夠配置 string 的物件

auto const p = alloc.allocate(n); //配置 n 固未建構的 string

對allocate的這個呼叫會為n個string配置記憶體。

頁482

3:23:10

Table 12.7. Standard allocator Class and Customized Algorithms

表12.7標準的allocator類和自訂的演算法

allocator<T> a 定義一個叫做a的allocator物件來為型別T的物件配置記憶體資源 Defines an allocator object named a that can allocate memory for objects of type T.

a.allocate(n)

疑當作:

auto p= a.allocate(n) 配置一個原初(raw)、尚未開發的(unconstructed,未經物件建構的)記憶體區塊(其實就是動態陣列的概念、模型)來放置(儲存hold)n個T型別的物件。

a.deallocate(p,n) 從p這個指向其型別的指標值(位址)位置開始,釋放n個T型別物件的記憶體資源。p必須是先前經由allocate成員函式運算回傳的指標,而n必須是當p被創建時被指定的那個大小值。在用到deallocate這個運算前,必須先對在這個記憶體區塊中被建構出來的物件呼叫destroy方法(destroy(p),詳本表末列)。



a.construct(p,args) p須是一個指標,指向一個型別為T、且為原生(raw memory)的記憶體區塊。引數args則會傳給T類別的建構器(constructor)用以建構出一個在p指向的記憶體區塊中的物件。

a.destroy(p) 在p所指向的物件上執行解構器(destructor,§12.1.1,頁452),p是一個指向T型別的指標(T* pointer p)。3:38:20其實p應即是指向元素型別T的普通指標

1:52:44 3:34:40

邏輯應該是:

1配置allocate 2建構construct3

3解構destroy 4釋放deallocate

3:39:10

allocators Allocate Unconstructed Memory

allocator型別物件配置的記憶體是尚未經物件建構過的記憶體(白地,未經開墾的白地)

4:33:20

一個allocator型別物件所配置出來的記憶體是未經建構初始化的(unconstructed)。因為allocator類別就是負責將建構(初始化)與配置分割開來的工作。

我們是藉由在該配置出來的記憶體區中建構物件來使用這些配置出來的記憶體的。所以沒有建構物件,就不能使用這個記憶體區,即使它配置成功了也一樣。(等於是一個懸置集區了)在新的程式庫中construct這個成員函式會帶有一個指標型別的參數、還有一組或有或無(可有可無)的額外參數。這個construct成員函式會在指定的記憶體位置上建構一個元素物件出來,額外的參數則是被用來初始化這個元素物件的。(可見這裡建構和初始化是分的;但若與「配置」來比的話,則建構與初始化是一國的)就像make_shared成員函式的參數(§12.1.1,頁451)這些額外的參數args必須是有效的初始器,以初始化這個被建構出來的型別物件元素。特別是,如果這個物件是一個類別型別(class type)的物件,那麼這些引數就必須和那個類別的建構器(constructor)相配合才行。

allocator<string> alloc;//宣告、定義一個allocator型別物件,它可以配置出一個存放string型別的記憶體資源(區塊)

object that can allocate strings

auto const p = alloc.allocate(n); //用alloc這個allocator型別物件來配置n個未經建構(unconstructed應即是未經初始化)的字串。

3:57:00 4:43:00 可見 allocate回傳的會是一個指向它配置出來的最後一個元素後的位置 5:08:00沒錄到的就請看臉書直播第439集了 7:18:02忘了按暫停了真的是實境秀了。呵呵。人在做,天在看,平生無不可告人者。感恩感恩 讚歎讚歎 南無阿彌陀佛 7:18:40

auto q = p; //q指標會指向最後一個被建構出來的元素後面的位置//此處決定有錯!英文版中文版均然。p應是指向allocate(n)配置出來的第一個元素,q才是指向最後一個元素後的位置,然auto q=p,用p將q初始化,不就是q=p了!

//第87集7:31:00原來這裡以下3行是分別表述的,只能擇一,不能連貫!

//3個都是q推進一個位置,故曰是「指向最後一個被建構出來的元素後面的位置」

//以下3行錯在是解參考p不是解參考q!英文版誤p為q!



alloc.construct(q++);//解參考(dereference)q就會得到一個空的string字串

alloc.construct(q++, 10, 'c'); //解參考q就會是一個10個c的字串字面值(string literal)(或string字串。因為是用string的建構器來建構的10個c的字串)

alloc.construct(q++, "hi"); //解參考q得到的是一個"hi"字串,乃是由string建構器構成的。

//要改成前綴版本,才能印出解參考q,並且後面destroy也要改成後綴版本,才不會出錯(誤刪空指標)「alloc.destroy(q--);」

alloc.construct(++q, "hi"); //解參考q得到的是一個"hi"字串,乃是由string建構器構成的。

在早期的程式庫版本,construct成員函式只定義了2個參數。一個是指標,指向的是要建構的元素物件所在位置,第二個是符合該元素物件型別的值。因此,在當時這種舊版construct的操作下,我們若想要建構一個元素物件,就只能將那個元素複製到未經建構的記憶體空間上,我們並不能使用元素型別的建構器—就像上述用了string類別的建構器—來建構那些元素。4:55:40

在早期版本的程式庫中,construct只接受兩個引數:要建構一個元素物件的指標,以及符合該元素物件型別的一個值。因此,我們只能拷貝一個元素到未經建構的記憶體空間中,無法使用該元素型別的其他建構器。【中文版照翻英文版,誰看得懂?】

要記住的是:企圖使用一個未曾有物件建構的原生記憶體區是決定錯誤的:因為沒有實體物件存在,如何「使用」?

cout << *p << endl; //可以:使用了string的輸出運算子(output operator)

cout << *q << endl; //阿災(災難,disaster,但實際在Visual Studio 2019此項並不會出錯!):q指向的是一個未經建構的記憶體區塊(就是q指向了一個不存在的物件,類似或等同於懸置指標)

警告:想要用到由allocate成員函式傳回的記憶體區域,我們就必須先用construct成員函式去建構好相關的物件。若是以其他方式企圖用到未經物件建構的記憶體區,都是毫無意義的行為。

因為只經過配置,而尚未實際建構與初始化任何物件,當然不能使用。使用的對象(object)也不會是記憶體本身,而是記憶體上存放的物件(object)。就如同用一個未經初始化的東西一樣,都是毫無意義的行徑。

當不再用到那些經由construct建構出來的物件時,就必須將它們摧毀。此時就需要調用destry成員函式來完成這個工作。destroy含有一個指標參數,會在這個指標指向的物件上呼叫對應的解構器(destructor,§12.1.1,頁452)來刪除元素物件以釋放其佔用的記憶體資源:

頁483

while (q != p)

alloc.destroy(--q); //釋放那些已經配置給string

可見動態配置的物件,必須一個個清除,動態陣列其元素亦復如此。

在上述迴圈的一開頭,q指向的是最後一個被建構出來的元素後的位置(因為q++遞增推移)。我們將它遞減(這裡前綴、後綴版本很關鍵,課本就誤倒了!),再調用destroy成員函式。所以第一次執行destroy函式,q所指向的就是最後一個建構出來的元素物件。(沒建構出來的,不能在其上呼叫destroy,會出錯!)而在最後一次迭代(iteration)時,就會對第一個建構出來的元素,使用destroy函式。8:0:00在這次迭代q指標遞減之後,q就會等於p,判斷式(q!=p)不成立迴圈就不再執行而結束了。可見p是指向第一元素的指標,q則是因為「q++」遞增推進,才會是指向p後的一個位置,而此例中,只有p位置上一個元素物件被建構(construct),故只有1個元素(size=1)。

警告:只能在建構好的元素物件上,調用destroy成員函式。

在我們這個實例中,一旦元素被摧毀,就可以再重新利用它曾佔用過的記憶體位置來存放其他的string物件(元素;這裡元素、物件是同一概念,抽換字面爾)或將之歸還給系統(system)。釋放記憶體的工作是交由deallocate成員函式來處理的:

alloc.deallocate(p,n);

和destroy成員函式一樣,指標引數都不可以是空值(null、nullptr)且指向的必須是由allocate成員函式配置出來的記憶體區塊(記憶體區段)。甚至另一個引數,傳入的值,其大小也必須和當初allocate配置給這個指標引數的記憶體時大小相同。

Algorithms to Copy and Fill Uninitialized Memory

前面一區段談記憶體配置,這裡談物件建構與初始化

複製和填充補滿未初始化記憶體的演算法

拷貝與填入未初始化的記憶體之演算法

8:20:00 8:25:00

對尚未初始化(建構物件、空無一物)的記憶體,進行拷貝、填滿物件的運算(演算)

Copy and Fill就是建構並初始化

8:31:10又是一個伙伴類別(附屬類別)

程式庫還定義了2個演算法(algorithm)來配合(as a companion to)allocator類別。這2個演算法可在未初始化的記憶體上建構出物件。在表12.8中列出的這兩個函式(演算法)都是定義在memory標頭檔中的。

Table 12.8 allocator Algorithms

These functions construct elements in the destination,rather than assigning to them. 10:47:50

這些函式是在目的記憶體上建構初始化元素而不是給這些元素指定值

uninitialized_copy(b,e,b2) 從一個由b、e兩個迭代器(iterator)指出的輸入範圍中將其元素值拷貝到尚未經任何構建、原生的記憶體區,這個記憶體區是由b2這個迭代器(iterator)來指出的,且以b2所指來作為其拷貝開始的位置。這個記憶體區必須大到容得下輸入範圍內的所有元素才行。

uninitialized_copy_n(b,n,b2) 從迭代器b所指示的位置開始,將n個元素拷貝到b2迭代器所指向的原生記憶體區的位置上。b2指向的是拷貝目的地的開始位置。

uninitialized_fill(b,e,t) 在原生記憶體區,由迭代器b與e指出的範圍上,將複製t來作為建構物件的初始器。

uninitialize_fill_n(b,n,t) 在迭代器b指向的原生記憶體區,以b為開始位置,將n個物件建構起來。這個記憶體區必須夠大才行。Constructs an unsigned number n objects starting at b. b must denote unconstructed, raw memory large enough to hold the given number of objects.這裡沒提到t這個參數,t應是用來建構n個物件的初始器。

t大概是承前省略了。

上表之b即begin,而b2就是第2個--也就是另一個begin(起始位置)。e即end,結束位置。

9:48:01

如果想將一個元素型別為int的vector,拷貝到動態記憶體(dynamic memory)上,我們得要配置兩倍大容量的記憶體區來納入所有在vector中的元素。我們須先在這個兩倍大的記憶體區的前半部,用拷貝的方式建構出所有vector內的元素,然後在後半部將這些建構出來的元素以它們相對應的值來進行初始化(填滿它們該有的值fill them with a given value):

我們會從原本的vector拷貝元素來建構新配置記憶體的前半部,而後半部的元素則是以一個給定值填入(fill)來建構

頁484

//配置一個兩倍大的記憶體區來存放vi容器中的元素,為什麼要2倍大?大概只是為了演示「copy、fill」這兩種演算法運作的實際,以作比較吧,並不是將vector拷貝到動態記憶體中都得配置2倍大的區塊才行!copy,是直接拷貝來源;而fill則只是建構如來源數量的物件(元素),然後再以特定的值來作為元素值,並不用來源物件的元素值;即用一個指定值來建構、拷貝。10:5:00

auto p = alloc.allocate(vi.size() * 2);

//在p指標指向的原生記憶體區的位置上,以該位置為起點,拷貝vi的元素來建構所有的元素物件//(allocate回傳的一個普通指標(plain pointer、ordinary pointer),普通指標是相對於智慧指標(smart pointer)來說的)

auto q = uninitialized_copy(vi.begin(), vi.end(), p);

//將其餘的元素物件初始化為42這個值,因為uninitialized_copy回傳一個指標,指向它拷貝終了的結束、末端位置。

uninitialized_fill_n(q, vi.size(), 42);

就像copy演算法(algorithm)(§10.2.2,頁382),uninitialized_copy也是回傳一個在拷貝目的地上已經經過遞增推進1個位置的迭代器。(回傳的是一個指標,這裡指標與迭代器又不分了)所以,uninitialize_copy的運算就會回傳一個指向拷貝目的地最後一個元素後面那個位置的指標。(在此例後面那個位置,仍是一個元素,是後半部的第1個元素。)

此處指標pointer和迭代器(iterator)又不分了,參見前表12.8)

我們在此將那個指標用q記下來(存到q裡頭),再將q當作引數傳給uninitialized_fill_n,作為標識填滿值的起始begin(b)位置。這個uninitialize_fill_n函式一如fill_n函式(§10.2.2,頁381),都帶了3個引數:

第1個引數是指向拷貝目的地的指標,也作為填滿元素值的起始位置(b,在此為q)

第2個指出要拷貝的數量(n,在此為vi.size())

第3個要填滿元素的值(t,在此為42)

uninitialized_fill_n會在目的的記憶體上由q(b)所指出的位置開始,以那個值(t)的引數值來建構(初始化)指定數量(n)的元素物件。

10:0:55 10:10:10

練習12.26

用allocator類別來重寫481頁上的那個將建構與配置結合的程式。10:41:00

size_t n; vector<string>vs;

allocator<string>alloc;

//string* const p = new string[n]; //建構n個空的string字串(預設初始化)



string s;

//while (cin >> s && q != p + n) //「cin>>s」:讀取標準輸入到s

while (cin >> s )

vs.push_back(s);

n = vs.size();

//p要在最後delete或destroy作為引數傳入,才能清除動態陣列,故p不能更動,才為const p

string* const p = alloc.allocate(n); //配置n個未初始化的string原生記憶體空間block(區塊、記憶體區塊)

// *q++ = s; //將一個新值(=s)指定給q指向的元素

//string* q = p; //q指向了p動態陣列中的第1個字串元素

string*q=uninitialized_copy(vs.cbegin(), vs.cend(), p); //可見並不需要2倍大的容量才能進行copy、fill,課文有誤導之嫌。在表12.8(頁483)也說只要是能容下來源數量的大小便可以了!

//const size_t size = q - p; //記下已經讀取了多少個動態陣列p中的字串元素

const size_t size = q - p;

if (size==n)

{

cout << "size==n" << endl;

}

//……

//對這個動態陣列的使用

q = p;

while (q != p + n)

cout<<*(q++)<<",";//++優先權比*高,加不加()是一樣的

cout << endl;

//delete[] p; //因為普通常值(恆定)指標(內建型別為對string的指標)p指向的是一個動態陣列,所以對它delete必須加上[]空的方括號

alloc.destroy(p);

https://github.com/oscarsun72/prog1-C-Primer-5th-Edition-s-Exercises/blob/exercise12_26/prog1/prog1.cpp

12.3. Using the Library: A Text-Query Program

12.3利用程式庫:做一個字串查詢的程式

作為對C++程式庫論述的總結,我們會實作一個檢易的字串查詢程式。這個程式可以讓使用者檢索文字檔(就是類似Word Find尋找功能)。檢索的結果會是一個數字及清單。數字表示該字詞在整個檔案中出現的次數;而清單則會陳列出要找的字詞所在該行的內容。如果要找的字在一行內出現多於1次,那行文字也只會列出一次,不會重複。且這個清單條列的方式也會以其各行的先後次序來作遞增排序。

舉例來說,如果我們是從一個包含了本章內容的檔案,試圖在其中找「element」這個字的話,那麼列出的結果前幾行應該會像這樣:

element occurs 112 times

(line 36) A set element contains only a key;

(line 158) operator creates a new element

(line 160) Regardless of whether the element

(line 168) When we fetch an element from a map, we

(line 214) If the element is not found, find returns

在此後則接著100多行都是element這個字出現的所在行。

後面會接著出現element這個字的剩餘的100多行。

頁485

11:17:20

12.3.1. Design of the Query Program

12.3.1此查詢程式之設計

這個檢索程式的設計 11:34:18

要開始設計一個程式前,最好是先能條列出這個程式會用到的所有操作/運算。知道我們需要什麼樣的運算就能夠明白我們需要怎樣的資料結構。以需求取向來考量,就知道我們這個檢索程式必須要完成如下的工作(達成如下的任務):

 在讀取資料的同時,程式必須記錄下檢索之字出現的那行文字。因此就必須借重一次讀取一行的功能,而且還要在讀取後,有效地將各個字詞從整行中分析出來。

 在輸出檢索結果的時候:

 程式必須能擷取到檢索字詞所在行的行號

 這些行號又必須按照遞增的方式來排序,且又不能重複

 程式要能將行號與其內容都呈現(print)出來

能善用程式庫提供的各種機能,這些需求就能得到很好的滿足

留言

熱門文章