• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            面對現實,超越自己
            逆水行舟,不進則退
            posts - 269,comments - 32,trackbacks - 0
                復雜的東西寫多了,如今寫點簡單的好了。由于功能上的需要,Vczh Library++3.0被我搞得很離譜。為了開發維護的遍歷、減少粗心犯下的錯誤以及增強單元測試、回歸測試和測試工具,因此記錄下一些開發上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。

                C++實謂各種語言中的軟肋,功能強大,陷阱更強大。當然我認為一門語言用得不好完全是程序員的責任,不過因為C++涉及到的概念實在是太多,想用好實在也不是一件容易的事情。C++開發的時候總是會遇到各種各樣的問題,其中最嚴重的無非是內存相關的。C語言由于結構簡單,內存處理起來雖然不得力,但總的來說慣用法已經深入人心,因此也不會造成什么很難發現的錯誤。C++就不一樣了。有了虛函數、構造函數、析構函數、復制構造函數和operator=重載之后,還是有很多人喜歡把一個類直接寫進文件流,或者拿來memset,代碼一團亂麻,不知悔改也。但是不能因此因噎廢食,就像某人因為C++帶來的心智問題太多,自己搞不定,自己團隊也搞不定,就說C++不好一樣。

                因此第一篇文章主要針對內存來講。我們處理內存,第一件事就是不要有內存泄露。內存泄露不能等到測試的時候,通過長時間運行程序并觀察任務管理器的方法來做,這顯然已經晚了。幸好Visual C++給了我們一個十分好用的工具:_CrtDumpMemoryLeaks函數。這個函數會在Debug模式下往Visual Studio的output窗口打印出那個時候你new(malloc)了但是還沒delete(free)的所有內存塊的地址、長度、前N個字節的內容和其他信息。怎么做呢?其實很簡單:
             1 #define _CRTDBG_MAP_ALLOC
             2 #include <stdlib.h>
             3 #include <crtdbg.h>
             4 #include <windows.h>
             5 
             6 int wmain(vint argc , wchar_t* args[])
             7 {
             8     // 這里運行程序,并在下面的函數調用之前delete掉所有new的東西
             9     _CrtDumpMemoryLeaks();
            10     return 0;
            11 }

                我們只需要在注釋的地方完成我們程序的功能,然后確信自己已經delete掉所有應該delete的東西,最后_CrtDumpMemoryLeaks()函數調用的時候就可以打印出沒被delete的東西了。這個方法十分神奇,因為你只需要在main函數所在的cpp文件這么#include一下,所有的cpp文件里面的new都會受到監視,跟平常所用的用宏把new給換掉的這種破方法截然不同。如果你使用了全局變量的話也要小心,因為全局變量的析構函數是在main函數結束之后才執行的,因此如果在全局變量的析構函數里面delete的東西仍然會被_CrtDumpMemoryLeaks函數當成泄露掉的資源對待。當然本人認為全局變量可以用,但是全局變量的賦值必須在main里面做,釋放也是,除非那個全局變量的構造函數沒有申請任何內存,所以這也是一個很好的檢查方法。

                不過上面也僅僅是一個告訴你有沒有內存泄漏的方法罷了。那么如何避免內存泄露呢?當然在設計一些性能要求沒有比操作系統更加嚴格的程序的時候,可以使用以下方法:
                1、如果構造函數new了一個對象并使用成員指針變量保存的話,那么必須在析構函數delete它,并且不能有為了某些便利而將這個對象的所有權轉讓出去的事情發生。
                2、在能使用shared_ptr的時候,盡量使用shared_ptr。shared_ptr只要你不發生循環引用,那么這個東西可以安全地互相傳遞、隨便你放在什么容器里面添加刪除、你想放哪里就放在哪里,再也不用考慮這個對象的生命周期問題了。
                3、不要在有構造函數和析構函數的對象上使用memset(或者memcpy)。如果一個對象需要memset,那么在該對象的構造函數里面memset自己。如果你需要memset一個對象數組,那也在該對象的構造函數里面memset自己。如果你需要memset一個沒有構造函數的復雜對象,那么請為他添加一個構造函數,除非那是別人的API提供的東西
                4、如果一個對象是繼承了其他東西,或者某些成員被標記了virtual的話,絕對不要memset。對象是獨立的,也就是說父類內部結構的演變不需要對子類負責。哪天父類里面加了一個string成員,被子類一memset,就欲哭無淚了。
                5、如果需要為一個對象定義構造函數,那么連復制構造函數、operator=重載和析構函數都全部寫全。如果不想寫復制構造函數和operator=的話,那么用一個空的實現寫在private里面,確保任何試圖調用這些函數的代碼都出現編譯錯誤。
                6、如果你實在很喜歡C語言的話,那麻煩換一個只支持C不支持C++的編譯器,全面杜絕因為誤用了C++而導致你的C壞掉的情況出現。

                什么是循環引用呢?如果兩個對象互相使用一個shared_ptr成員變量直接或者間接指向對方的話,就是循環引用了。在這種情況下引用計數會失效,因為就算外邊的shared_ptr全釋放光了,引用計數也不會是0的。

                今天就說到這里了,過幾天我高興的話再寫一篇續集,如果我持續高興的話呢……嗯嗯……。

                復雜的東西寫多了,如今寫點簡單的好了。由于功能上的需要,Vczh Library++3.0被我搞得很離譜。為了開發維護的遍歷、減少粗心犯下的錯誤以及增強單元測試、回歸測試和測試工具,因此記錄下一些開發上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。

                上一篇文章講到了如何檢查內存泄露。其實只要肯用C++的STL里面的高級功能的話,內存泄露是很容易避免的。我在開發Vczh Library++ 3.0的時候,所有的測試用例都保證跑完了沒有內存泄露。但是很可惜有些C++團隊不能使用異常,更甚者不允許寫構造函數析構函數之類,前一個還好,后一個簡直就是在用C。當然有這些變態規定的地方STL都是用不了的,所以我們更加需要扎實的基礎來開發C++程序。

                今天這一篇主要還是講指針的問題。因為上一篇文章一筆帶過,今天就來詳細講內存泄漏或者野指針發生的各種情況。當然我不可能一下子舉出全部的例子,只能說一些常見的。

                一、錯誤覆蓋內存。

                之前提到的不能隨便亂memset其實就是為了避免這個問題的。其實memcpy也不能亂用,我們來看一個例子,最簡單的:
             1 #define MAX_STRING 20;
             2 
             3 struct Student
             4 {
             5   char name[MAX_STRING];
             6   char id[MAX_STRING];
             7   int chinese;
             8   int math;
             9   int english;
            10 };

                大家對這種結構肯定十分熟悉,畢竟是大學時候經常要寫的作業題……好了,大家很容易看得出來這其實是C語言的經典寫法。我們拿到手之后,一般會先初始化一下,然后賦值。
            1 Student vczh;
            2 memset(&vczh, 0sizeof(vczh));
            3 strcpy(vczh.name, "vczh");
            4 strcpy(vczh.id, "VCZH'S ID");
            5 vczh.chinese=70;
            6 vczh.math=90;
            7 vczh.english=80;

                為什么要在這里使用memset呢?memset的用處是將一段內存的每一個字節都設置成同一個數字。這里是0,因此兩個字符串成員的所有字節都會變成0。因此在memset了Student之后,我們通過正常方法來訪問name和id的時候都會得到空串。而且如果Student里面有指針的話,0指針代表的是沒有指向任何有效對象,因此這個時候對指針指向的對象進行讀寫就會立刻崩潰。對于其他數值,0一般作為初始值也不會有什么問題(double什么的要小心)。這就是我們寫程序的時候使用memset的原因。

                好了,如今社會進步,人民當家做主了,死程們再也不需要受到可惡的C語言剝削了,我們可以使用C++!因此我們借助STL的力量把Student改寫成下面這種帶有C++味道的形式:
            1 struct Student
            2 {
            3   std::string name;
            4   std::string id;
            5   int chinese;
            6   int math;
            7   int english;
            8 };

                我們仍然需要對Student進行初始化,不然三個分數還是隨機值。但是我們又不想每一次創建的時候都對他們分別進行賦值初始化城0。這個時候你心里可能還是想著memset,這就錯了!在memset的時候,你會把std::string內部的不知道什么東西也給memset掉。假如一個空的std::string里面存放的指針指向的是一個空的字符串而不是用0來代表空的時候,一下子內部的指針就被你刷成0,等下std::string的析構函數就沒辦法delete掉指針了,于是內存泄露就出現了。有些朋友可能不知道上面那句話說的是什么意思,我們現在來模擬一下不能memset的std::string要怎么實現。

                為了讓memset一定出現內存泄露,那么std::string里面的指針必須永遠都指向一個有效的東西。當然我們還需要在字符串進行復制的時候復制指針。我們這里不考慮各種優化技術,用最簡單的方法做一個字符串出來:
             1 class String
             2 {
             3 private:
             4   char* buffer;
             5 
             6 public:
             7   String()
             8   {
             9     buffer=new char[1];
            10     buffer[0]=0;
            11   }
            12 
            13   String(const char* s)
            14   {
            15     buffer=new char[strlen(s)+1];
            16     strcpy(buffer, s);
            17   }
            18 
            19   String(const String& s)
            20   {
            21     buffer=new char[strlen(s.buffer)+1];
            22     strcpy(buffer, s.buffer);
            23   }
            24 
            25   ~String()
            26   {
            27     delete[] buffer;
            28   }
            29 
            30   String& operator=(const String& s)
            31   {
            32     delete[] buffer;
            33     buffer=new char[strlen(s.buffer)+1];
            34     strcpy(buffer, s.buffer);
            35   }
            36 };

                于是我們來做一下memset。首先定義一個字符串變量,其次memset掉,讓我們看看會發生什么事情:
            1 string s;
            2 memset(&s, 0sizeof(s));

                第一行我們構造了一個字符串s。這個時候字符串的構造函數就會開始運行,因此strcmp(s.buffer, "")==0。第二行我們把那個字符串給memset掉了。這個時候s.buffer==0。于是函數結束了,字符串的析構函數嘗試delete這個指針。我們知道delete一個0是不會有問題的,因此程序不會發生錯誤。我們活生生把構造函數賦值給buffer的new char[1]給丟了!鐵定發生內存泄露!

                好了,提出問題總要解決問題,我們不使用memset的話,怎么初始化Student呢?這個十分好做,我們只需要為Student加上構造函數即可:
            1 struct Student
            2 {
            3   .//不重復那些聲明
            4 
            5   Student():chinese(0),math(0),english(0)
            6   {
            7   }
            8 };

                這樣就容易多了。每當我們定義一個Student變量的時候,所有的成員都初始化好了。name和id因為string的構造函數也自己初始化了,因此所有的成員也都初始化了。加入Student用了一半我們想再初始化一下怎么辦呢?也很容易:
            1 Student vczh;
            2 .//各種使用
            3 vczh=Student();

                經過一個等號操作符的調用,舊Student的所有成員就被一個新的初始化過的Student給覆蓋了,就如同我們對一個int變量重新賦值一樣常見。當然因為各種復制經常會出現,因此我們也要跟上面貼出來的string的例子一樣,實現好那4個函數。至此我十分不理解為什么某些團隊不允許使用構造函數,我猜就是為了可以memset,其實是很沒道理的。

                二、異常。

                咋一看內存泄露跟異常好像沒什么關系,但實際上這種情況更容易發生。我們來看一個例子:
             1 char* strA=new char[MAX_PATH];
             2 if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
             3 char* strB=new char[MAX_PATH];
             4 if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
             5 
             6 DoSomething(strA, strB);
             7 
             8 RELEASE_STRB:
             9 delete[] strB;
            10 RELEASE_STRA:
            11 delete[] strA;

                相信這肯定是大家的常用模式。我在這里也不是教唆大家使用goto,不過對于這種例子來說,用goto是最優美的解決辦法了。但是大家可以看出來,我們用的是C++,因為這里有new。如果DoSomething發生了異常怎么辦呢?如果GetXXX發生了異常怎么辦呢?我們這里沒有任何的try-catch,一有異常,函數里克結束,兩行可憐的delete就不會被執行到了,于是內存泄漏發生了

                那我們如何避免這種情況下的內存泄露呢?一些可愛的小盆友可能會想到,既然是因為沒有catch異常才發生的內存泄露,那我們來catch吧:
             1 char* strA=new char[MAX_PATH];
             2 try
             3 {
             4   if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
             5   char* strB=new char[MAX_PATH];
             6   try
             7   {
             8     if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
             9     DoSomething(strA, strB);
            10   }
            11   catch()
            12   {
            13     delete[] strB;
            14     throw;
            15   }
            16 }
            17 catch()
            18 {
            19   delete[] strA;
            20   throw;
            21 }
            22 
            23 RELEASE_STRB:
            24 delete[] strB;
            25 RELEASE_STRA:
            26 delete[] strA;

                你能接受嗎?當然是不能的。問題出在哪里呢?因為C++沒有try-finally。你看這些代碼到處都是雷同的東西,顯然我們需要編譯器幫我們把這些問題搞定。最好的解決方法是什么呢?顯然還是構造函數和析構函數。總之記住,如果想要事情成對發生,那么使用構造函數和析構函數

                第一步,GetXXX顯然只能支持C模式的東西,因此我們要寫一個支持C++的:
             1 bool GetXXX2(string& s)
             2 {
             3   char* str=new char[MAX_PATH];
             4   bool result;
             5   try
             6   {
             7     result=GetXXX(str, MAX_PATH);
             8     if(result)s=str;
             9   }
            10   catch()
            11   {
            12     delete[] str;
            13     throw;
            14   }
            15   delete[] str;
            16   return result;
            17 }

                借助這個函數我們可以看到,因為有了GetXXX這種C的東西,導致我們多了多少麻煩。不過這總是一勞永逸的,有了GetXXX2和修改之后的DoSomething2之后,我們就可以用更簡單的方法來做了:
            1 string a,b;
            2 if(GetXXX2(a) && GetXXX2(b))
            3 {
            4   DoSomething2(a, b);
            5 }

                多么簡單易懂。這個代碼在任何地方發生了異常,所有new的東西都會被delete。這就是析構函數的一個好處。一個變量的析構函數在這個變量超出了作用域的時候一定會被調用,無論代碼是怎么走出去的。

                今天就說到這里了。說了這么多還是想讓大家不要小看構造函數和析構函數。那種微不足道的因為一小部分不是瓶頸的性能問題而放棄構造函數和析構函數的做法,終究是要為了修bug而加班的。只要明白并用好了構造函數、析構函數和異常,那么C++的特性也可以跟C一樣清楚明白便于理解,而且寫出來的代碼更好看的。大家期待第三篇哈。

                復雜的東西寫多了,如今寫點簡單的好了。由于功能上的需要,Vczh Library++3.0被我搞得很離譜。為了開發維護的遍歷、減少粗心犯下的錯誤以及增強單元測試、回歸測試和測試工具,因此記錄下一些開發上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。

                今天是關于內存的最后一篇了。上一篇文章講了為什么不能對一個東西隨便memset。里面的demo代碼出了點小bug,不過我不喜歡在發文章的時候里面的demo代碼也拿去編譯和運行,所以大家有什么發現的問題就評論吧。這樣也便于后來的人不會受到誤導。這次說的仍然是構造函數和析構函數的事情,不過我們將通過親手開發一個智能指針的方法,知道引用計數如何幫助管理資源,以及錯誤使用引用計數的情況。

                首先先來看一下智能指針是如何幫助我們管理內存的。現在智能指針的實現非常多,我就假設這個類型叫Ptr<T>吧。這跟Vczh Library++ 3.0所使用的實現一樣。

             1 class Base
             2 {
             3 public:
             4   virtual ~Base(){}
             5 };
             6 
             7 class Derived1 : public Base
             8 {
             9 };
            10 
            11 class Derived2 : public Base
            12 {
            13 };
            14 
            15 //---------------------------------------
            16 
            17 List<Ptr<Base>> objects;
            18 objects.Add(new Derived1);
            19 objects.Add(new Derived2);
            20 
            21 List<Ptr<Base>> objects2;
            22 objects2.Add(objects[0]);

                當然這里的List也是Vczh Library++3.0實現的,不過這玩意兒跟vector也好跟C#的List也好都是一個概念,因此也就不需要多加解釋了。我們可以看到智能指針的一個好處,只要沒有循環引用出現,你無論怎么復制它,最終總是可以被析構掉的。另一個例子告訴我們智能指針如何處理類型轉換:
            1 Ptr<Derived1> d1=new Derived1;
            2 Ptr<Base> b=d1;
            3 Ptr<Derived2> d2=b.Cast<Derived2>();
            4 // d2是空,因為b指向的是Derived1而不是Derived2。

                這就如同我們Derived1*可以隱式轉換到Base*,而當你使用dynamic_cast<Derived2*>(static_cast<Base*>(new Derived1))會得到0一樣。智能指針在幫助我們析構對象的同時,也要做好類型轉換的工作。

                好了,現在先讓我們一步一步做出那個Ptr<T>。我們需要清楚這個智能指針所要實現的功能是什么,然后我們一個一個來做。首先讓我們列出一張表:
                1、沒有參數構造的時候,初始化為空
                2、使用指針構造的時候,擁有那個指針,并且在沒有任何智能指針指向那個指針的時候刪除掉該指針。
                3、智能指針進行復制的時候,兩個智能指針共同擁有該內部指針。
                4、智能指針可以使用新的智能指針或裸指針重新賦值。
                5、需要支持隱式指針類型轉換,static_cast不支持而dynamic_cast支持的轉換則使用Cast<T2>()成員函數來解決。
                6、如果一個裸指針直接用來創建兩個智能指針的話,期望的情況是當兩個智能指針析構掉的時候,該指針會被delete兩次從而崩潰。
                7、不處理循環引用。

                最后兩點實際上是錯誤使用智能指針的最常見的兩種情況。我們從1到5一個一個實現。首先是1。智能指針可以隱式轉換成bool,可以通過operator->()拿到內部的T*。在沒有使用參數構造的時候,需要轉換成false,以及拿到0:
             1 template<typename T>
             2 class Ptr
             3 {
             4 private:
             5   T* pointer;
             6   int* counter;
             7 
             8   void Increase()
             9   {
            10     if(counter)++*counter;
            11   }
            12 
            13   void Decrease()
            14   {
            15     if(counter && --*counter==0)
            16     {
            17       delete counter;
            18       delete pointer;
            19       counter=0;
            20       pointer=0;
            21     }
            22   }
            23 
            24 public:
            25   Ptr():pointer(0),counter(0)
            26   {
            27   }
            28 
            29   ~Ptr()
            30   {
            31     Decrease();
            32   }
            33 
            34   operator bool()const
            35   {
            36     return counter!=0;
            37   }
            38 
            39   T* operator->()const
            40   {
            41     return pointer;
            42   }
            43 };

                在這里我們實現了構造函數和析構函數。構造函數把內部指針和引用計數的指針都初始化為空,而析構函數則進行引用計數的減一操作。另外兩個操作符重載很容易理解。我們主要來看看Increase函數和Decrease函數都分別做了什么。Increase函數在引用計數存在的情況下,把引用計數加一。而Decrease函數在引用計數存在的情況下,把引用計數減一,如果引用計數在減一過程中變成了0,則刪掉擁有的資源。

                當然到了這個時候智能指針還不能用,我們必須替他加上復制構造函數,operator=操作符重載以及使用指針賦值的情況。首先讓我們來看使用指針賦值的話我們應該加上什么:
             1   Ptr(T* p):pointer(0),counter(0)
             2   {
             3     *this=p;
             4   }
             5 
             6   Ptr<T>& operator=(T* p)
             7   {
             8     Decrease();
             9     if(p)
            10     {
            11       pointer=p;
            12       counter=new int(1);
            13     }
            14     else
            15     {
            16       pointer=0;
            17       counter=0;
            18     }
            19     return *this;
            20   }

                這里還是偷工減料了的,構造函數接受了指針的話,還是轉給operator=去調用了。當一個智能指針被一個新指針賦值的時候,我們首先要減掉一個引用計數,因為原來的指針再也不被這個智能指針共享了。之后就進行判斷,如果來的是0,那么就變成空。如果不是0,就擁有該指針,引用計數初始化成1。于是我們就可以這么使用了:
            1 Ptr<Base> b=new Derived1;
            2 Ptr<Derived2> d2=new Derived2;

                讓我們開始復制他們吧。復制的要領是,先把之前擁有的指針脫離掉,然后連接到一個新的智能指針上面去。我們知道非空智能指針有多少個,總的引用計數的和就是多少,只是分配到各個指針上面的數字不一樣而已:
             1   Ptr(const Ptr<T>& p):pointer(p.pointer),counter(p.counter)
             2   {
             3     Increase();
             4   }
             5 
             6   Ptr<T>& operator=(const Ptr<T>& p)
             7   {
             8     if(this!=&p)
             9     {
            10       Decrease();
            11       pointer=p.pointer;
            12       counter=p.counter;
            13       Increase();
            14     }
            15     return *this;
            16   }

                在上一篇文章有朋友指出重載operator=的時候需要考慮是不是自己賦值給自己,其實這是很正確的。我們寫每一類的時候,特別是當類擁有自己控制的資源的時候,需要非常注意這件事情。當然如果只是復制幾個對象而不會new啊delete還是close什么handle,那檢查不檢查也無所謂了。在這里我們非常清楚,當增加一個新的非空智能指針的時候,引用計數的總和會加一。當修改一個非空智能指針的結果也是非空的時候,引用計數的和保持不變。當然這是應該的,因為我們需要在所有非空智能指針都被毀掉的時候,釋放受保護的所有資源。

                到了這里一個智能指針基本上已經能用了,但是還不能處理父類子類的情況。這個是比較麻煩的,一個Ptr<Derived>事實上沒有權限訪問Ptr<Base>的內部對象。因此我們需要通過友元類來解決這個問題。現在讓我們來添加兩個新的函數吧,從一個任意的Ptr<C>復制到Ptr<T>,然后保證只有當C*可以隱式轉換成T*的時候編譯能夠通過:
             1   template<X> friend class Ptr;
             2 
             3   template<typename C>
             4   Ptr(const Ptr<C>& p):pointer(p.pointer),counter(p.counter)
             5   {
             6     Increase();
             7   }
             8 
             9   template<typename C>
            10   Ptr<T>& operator=(const Ptr<C>& p)
            11   {
            12     Decrease();
            13     pointer=p.pointer;
            14     counter=p.counter;
            15     Increase();
            16     return *this;
            17   }

                注意這里我們的operator=并不用檢查是不是自己給自己賦值,因為這是兩個不同的類,相同的話會調用上面那個operator=的。如果C*不能隱式轉換到T*的話,這里的pointer=p.pointer就會失敗,從而滿足了我們的要求。

                現在我們能夠做的事情就更多了:
            1 Ptr<Derived1> d1=new Derived1;
            2 Ptr<Base> b=d1;

                于是我們只剩下最后一個Cast函數了。這個函數內部使用dynamic_cast來做判斷,如果轉換失敗,會返回空指針:
             1   tempalte<typename C>
             2   Ptr<C> Cast()const
             3   {
             4     C* converted=dynamic_cast<C*>(pointer);
             5     Ptr<C> result;
             6     if(converted)
             7     {
             8       result.pointer=converted;
             9       result.counter=counter;
            10       Increase();
            11     }
            12     return result;
            13   }

                這是一種hack的方法,平時是不鼓勵的……不過因為操作的都是Ptr,而且特化Ptr也是使用錯誤的一種,所以這里就不管了。我們會檢查dynamic_cast的結果,如果成功了,那么會返回一個非空的新智能指針,而且這個時候我們也要記住Increase一下。

                好了,基本功能就完成了。當然一個智能指針還要很多其他功能,譬如說比較什么的,這個就你們自己搞定哈。

                指針和內存就說到這里了,下一篇講如何利用一個好的IDE構造輕量級單元測試系統。我們都說好的工具能夠提高生產力,因此這種方法不能脫離一個好的IDE使用。

            本文轉自:http://www.shnenglu.com/vczh/category/14099.html
            posted on 2012-09-12 14:51 王海光 閱讀(500) 評論(0)  編輯 收藏 引用 所屬分類: C++
            久久免费小视频| 情人伊人久久综合亚洲| 色播久久人人爽人人爽人人片aV | 日本精品一区二区久久久| 伊色综合久久之综合久久| 久久久久AV综合网成人 | 一个色综合久久| 久久99精品国产麻豆| 99久久精品无码一区二区毛片| 国内精品久久久久久久涩爱| 少妇内射兰兰久久| 国产精品99久久久久久www| 国产精品中文久久久久久久| 91久久精品国产91性色也| 久久午夜无码鲁丝片秋霞| 国产精品无码久久四虎| 久久精品亚洲日本波多野结衣| 日本高清无卡码一区二区久久| 久久99国产精一区二区三区| 伊人久久大香线蕉av一区| 狠狠人妻久久久久久综合| 久久这里只有精品18| 久久精品综合网| 亚洲AⅤ优女AV综合久久久| 久久这里只有精品首页| 无遮挡粉嫩小泬久久久久久久| 亚洲精品乱码久久久久久不卡| 国产毛片久久久久久国产毛片 | 亚洲AV无码久久精品蜜桃| 中文成人久久久久影院免费观看| 91精品国产91久久久久久| 韩国三级大全久久网站| 久久精品www人人爽人人| 久久午夜羞羞影院免费观看| 精品无码久久久久国产动漫3d| 模特私拍国产精品久久| 色诱久久av| 日产精品久久久久久久| 精品无码久久久久久午夜| 精品熟女少妇a∨免费久久| 2021久久国自产拍精品|