日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

C语言运行时库详解

發布時間:2023/12/20 编程问答 60 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C语言运行时库详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

網址:http://blog.csdn.net/jxth152913/archive/2010/07/02/5708369.aspx

運行時庫是程序在運行時所需要的庫文件,通常運行時庫是以LIB或DLL形式提供的。C運行時庫誕生于20世紀70年代,當時的程序世界還很單純,應用程序都是單線程的,多任務或多線程機制在此時還屬于新觀念。所以這個時期的C運行時庫都是單線程的。

  隨著操作系統?多線程技術的發展?,最初的C運行時庫無法滿足程序的需求,出現了嚴重的問題?。C運行時庫使用了多個全局變量(例如errno)和靜態變量,這可能在多線程程序中引起沖突。假設兩個線程都同時設置errno,其結果是后設置的errno會將先前的覆蓋,用戶得不到正確的錯誤信息。

  因此,Visual C++提供了兩種版本的C運行時庫。一個版本供單線程應用程序調用,另一個版本供多線程應用程序調用。多線程運行時庫與單線程運行時庫有兩個重大差別:

  (1)類似errno的全局變量,每個線程單獨設置一個;

  這樣從每個線程中可以獲取正確的錯誤信息。

  (2)多線程庫中的數據結構以同步機制加以保護。

  這樣可以避免訪問時候的沖突。

  Visual C++提供的多線程運行時庫又分為靜態鏈接庫和動態鏈接庫兩類,而每一類運行時庫又可再分為debug版和release版,因此Visual C++共提供了6個運行時庫。如下表:

C運行時庫 庫文件
Single thread(static link) ?libc.lib
Debug single thread(static link) ?libcd.lib
MultiThread(static link) ?libcmt.lib
Debug multiThread(static link) libcmtd.lib
MultiThread(dynamic link) msvert.lib
Debug multiThread(dynamic link) msvertd.lib?

  2.C運行時庫的作用

  C運行時庫除了給我們提供必要的庫函數調用(如memcpy、printf、malloc等)之外,它提供的另一個最重要的功能是為應用程序添加啟動函數。

  C運行時庫啟動函數的主要功能為進行程序的初始化,對全局變量進行賦初值,加載用戶程序的入口函數。

  不采用寬字符集的控制臺程序的入口點為mainCRTStartup(void)。下面我們以該函數為例來分析運行時庫究竟為我們添加了怎樣的入口程序。這個函數在crt0.c中被定義,下列的代碼經過了筆者的整理和簡化:

  • void mainCRTStartup(void)
  • {
  •  int mainret;
  •  /*獲得WIN32完整的版本信息*/
  •  _osver = GetVersion();
  •  _winminor = (_osver >> 8) & 0x00FF ;
  •  _winmajor = _osver & 0x00FF ;
  •  _winver = (_winmajor << 8) + _winminor;
  •  _osver = (_osver >> 16) & 0x00FFFF ;
  •  _ioinit(); /* initialize lowio */
  •  /* 獲得命令行信息 */
  •  _acmdln = (char *) GetCommandLineA();
  •  /* 獲得環境信息 */
  •  _aenvptr = (char *) __crtGetEnvironmentStringsA();
  •  _setargv(); /* 設置命令行參數 */
  •  _setenvp(); /* 設置環境參數 */
  •  _cinit(); /* C數據初始化:全局變量初始化,就在這里!*/
  •  __initenv = _environ;
  •  mainret = main( __argc, __argv, _environ ); /*調用main函數*/
  •  exit( mainret );
  • }
  • 復制代碼

    從以上代碼可知,運行庫在調用用戶程序的main或WinMain函數之前,進行了一些初始化工作。初始化完成后,接著才調用了我們編寫的main或WinMain函數。只有這樣,我們的C語言運行時庫和應用程序才能正常地工作起來。

      除了crt0.c外,C運行時庫中還包含wcrt0.c、 wincrt0.c、wwincrt0.c三個文件用來提供初始化函數。wcrt0.c是crt0.c的寬字符集版,wincrt0.c中包含 windows應用程序的入口函數,而wwincrt0.c則是wincrt0.c的寬字符集版。

      Visual C++的運行時庫源代碼缺省情況下不被安裝。如果您想查看其源代碼,則需要重裝Visual C++,并在重裝在時選中安裝運行庫源代碼選項。

      3.各種C運行時庫的區別

      (1)靜態鏈接的單線程庫

      靜態鏈接的單線程庫只能用于單線程的應用程序,C運行時庫的目標代碼最終被編譯在應用程序的二進制文件中。通過/ML編譯選項可以設置Visual C++使用靜態鏈接的單線程庫。

      (2)靜態鏈接的多線程庫

      靜態鏈接的多線程庫的目標代碼也最終被編譯在應用程序的二進制文件中,但是它可以在多線程程序中使用。通過/MD編譯選項可以設置Visual C++使用靜態鏈接的單線程庫。

      (3)動態鏈接的運行時庫

      動態鏈接的運行時庫將所有的C庫函數保存在一個單獨的動態鏈接庫MSVCRTxx.DLL中,MSVCRTxx.DLL處理了多線程問題。使用/ML編譯選項可以設置Visual C++使用動態鏈接的運行時庫。

      /MDd、 /MLd 或 /MTd 選項使用 Debug runtime library(調試版本的運行時刻函數庫),與/MD、 /ML 或 /MT分別對應。Debug版本的 Runtime Library 包含了調試信息,并采用了一些保護機制以幫助發現錯誤,加強了對錯誤的檢測,因此在運行性能方面比不上Release版本。

      下面看一個未正確使用C運行時庫的控制臺程序:

  • #include <stdio.h>
  • #include <afx.h>
  • int main()
  • {
  •  CFile file;
  •  CString str("I love you");
  •  TRY
  •  {
  •   file.Open("file.dat",CFile::modeWrite | CFile::modeCreate);
  •  }
  •  CATCH( CFileException, e )
  •  {
  •   #ifdef _DEBUG
  •   afxDump << "File could not be opened " << e->m_cause << "/n";
  •   #endif
  •  }
  •  END_CATCH
  •  file.Write(str,str.GetLength());
  •  file.Close();
  • }
  • 復制代碼

    我們在"rebuild all"的時候發生了link錯誤:

  • nafxcwd.lib(thrdcore.obj) : error LNK2001: unresolved external symbol __endthreadex
  • nafxcwd.lib(thrdcore.obj) : error LNK2001: unresolved external symbol __beginthreadex
  • main.exe : fatal error LNK1120: 2 unresolved externals
  • Error executing cl.exe.
  • 復制代碼

    發 生錯誤的原因在于Visual C++對控制臺程序默認使用單線程的靜態鏈接庫,而MFC中的CFile類已暗藏了多線程。我們只需要在Visual C++6.0中依次點選Project->Settings->C/C++菜單和選項,在Project Options里修改編譯選項即可。

    C 運行時庫是微軟?對 標準C庫函數的實現,因為當時考慮到許多程序都使用C編寫,而這些程序都要使用標準的C庫,按照以前的方式每一個程序最終都要拷貝一份標準庫的實現到程序 中,這樣同一時刻內存中可能有許多份標準庫的代碼(一個程序一份),所以微軟出于效率的考慮把 ?標準C庫做為動態鏈接來實現,這樣多個程序使用C標準庫時內存中就只有一份拷貝了。(對每一個程序來說,它相當于自己擁有一份,? ?對于標準庫中的全局變量也做了處理的,不會因為共享同一份代碼而出現沖突)。? ?這也算是對C標準庫的一個擴展吧,至于說靜態鏈接的時候仍然把它叫做運行時庫那只能說這是個習慣問題而已了。??
    ?
    ?運行時庫和普通的? ?dll? ?一樣,如果有程序用到了才會加載,沒有程序使用的時候不會駐留內存的。話雖如此,但有多少系統的東西說不定也是用C寫的,這些東西的存在就使C運行時庫存在于內存中了

    從字面上看,運行庫是程序在運行時所需要的庫文件。通常運行庫是以DLL形式提供的。 Delphi和C++ Builder的運行庫為.bpl文件,實際還是一個DLL。運行庫中一般包括編程時常用的函數,如字符串操作、文件操作、界面等內容。不同的語言所支持 的函數通常是不同的,所以使用的庫也是完全不同的,這就是為什么有VB運行庫、C運行庫、Delphi運行庫之分的原因。即使都是C++語言,也可能因為 提供的函數不同,而使用不同的庫。如VC++使用的運行庫和C++ Builder就完全不同。
    如果不使用運行庫,每個程序中都會包括很多重復的代碼,而使用運行庫,可以大大縮小編譯后的程序的大小。但另一方面,由于使用了運行庫,所以在分發程序時就必須帶有這些庫,比較麻煩。如果在操作系統中找不到相應的運行庫程序就無法運行。為了解決這個矛盾,Windows?總 是會帶上它自己開發的軟件的最新的運行庫。象Windows 2000以后的版本都包括Visual Basic 5.0/6.0的庫。Internet Explorer總是帶有最新的Visual C++ 6.0的庫。Windows XP帶有Microsoft .NET 1.0(用于VB.NET和C#)的庫。Visual C++、Delphi和C++ Builder允許用戶選擇所編譯得到的程序是否依賴于運行庫。而VB、FoxPro、PowerBuilder、LabWindows/CVI和 Matlab就不允許用戶進行這種選擇,必須依賴于運行庫。

    2 內存泄漏

    2.1 C++中動態內存分配引發問題的解決方案

    假設我們要開發一個String類,它可以方便地處理字符串數據。我們可以在類中聲明一個數組,考慮到有時候字符串極長,我們可以把數組大小設為200, 但一般的情況下又不需要這么多的空間,這樣是浪費了內存。對了,我們可以使用new操作符,這樣是十分靈活的,但在類中就會出現許多意想不到的問題,本文 就是針對這一現象而寫的。現在,我們先來開發一個String類,但它是一個不完善的類。的確,我們要刻意地使它出現各種各樣的問題,這樣才好對癥下藥。 好了,我們開始吧!

  • /* String.h */
  • #ifndef STRING_H_
  • #define STRING_H_
  • class String
  • {
  • private:
  • char * str; //存儲數據
  • int len; //字符串長度
  • public:
  • String(const char * s); //構造函數
  • String(); // 默認構造函數
  • ~String(); // 析構函數
  • friend ostream & operator<<(ostream & os,const String& st);
  • };
  • #endif
  • 復制代碼
  • /*String.cpp*/
  • #include <iostream>
  • #include <cstring>
  • #include "String.h"
  • using namespace std;
  • String::String(const char * s)
  • {
  • len = strlen(s);
  • str = new char[len + 1];
  • strcpy(str, s);
  • }//拷貝數據
  • String::String()
  • {
  • len =0;
  • str = new char[len+1];
  • str[0]='"0';
  • }
  • String::~String()
  • {
  • cout<<"這個字符串將被刪除:"<<str<<'"n';//為了方便觀察結果,特留此行代碼。
  • delete [] str;
  • }
  • ostream & operator<<(ostream & os, const String & st)
  • {
  • os << st.str;
  • return os;
  • }
  • 復制代碼
  • /*test_right.cpp*/
  • #include <iostream>
  • #include <stdlib.h>
  • #include "String.h"
  • using namespace std;
  • int main()
  • {
  • String temp("大家網");
  • cout<<temp<<'"n';
  • system("PAUSE");
  • return 0;
  • }
  • 復制代碼

    運行結果:

      大家網

      請按任意鍵繼續. . .

      大家可以看到,以上程序十分正確,而且也是十分有用的。可是,我們不能被表面現象所迷惑!下面,請大家用test_String.cpp文件替換test_right.cpp文件進行編譯,看看結果。有的編譯器可能就是根本不能進行編譯!

  • /*test_String.cpp:*/
  • #include <iostream>
  • #include <stdlib.h>
  • #include "String.h"
  • using namespace std;
  • void show_right(const String&);
  • void show_String(const String);//注意,參數非引用,而是按值傳遞。
  • int main()
  • {
  • String test1("第一個范例。");
  • String test2("第二個范例。");
  • String test3("第三個范例。");
  • String test4("第四個范例。");
  • cout<<"下面分別輸入三個范例:"n";
  • cout<<test1<<endl;
  • cout<<test2<<endl;
  • cout<<test3<<endl;
  • String* String1=new String(test1);
  • cout<<*String1<<endl;
  • delete String1;
  • cout<<test1<<endl; //在Dev-cpp上沒有任何反應。
  • cout<<"使用正確的函數:"<<endl;
  • show_right(test2);
  • cout<<test2<<endl;
  • cout<<"使用錯誤的函數:"<<endl;
  • show_String(test2);
  • cout<<test2<<endl; //這一段代碼出現嚴重的錯誤!
  • String String2(test3);
  • cout<<"String2: "<<String2<<endl;
  • String String3;
  • String3=test4;
  • cout<<"String3: "<<String3<<endl;
  • cout<<"下面,程序結束,析構函數將被調用。"<<endl;
  • return 0;
  • }
  • void show_right(const String& a)
  • {
  • cout<<a<<endl;
  • }
  • void show_String(const String a)
  • {
  • cout<<a<<endl;
  • }
  • 復制代碼

    運行結果:

      下面分別輸入三個范例:

      第一個范例。

      第二個范例。

      第三個范例。

      第一個范例。

      這個字符串將被刪除:第一個范例。

      使用正確的函數:

      

      第二個范例。

      第二個范例。

      使用錯誤的函數:

      第二個范例。

      這個字符串將被刪除:第二個范例。

      這個字符串將被刪除:?=

      ?=

      String2: 第三個范例。

      String3: 第四個范例。

      下面,程序結束,析構函數將被調用。

      這個字符串將被刪除:第四個范例。

      這個字符串將被刪除:第三個范例。

      這個字符串將被刪除:?=

      這個字符串將被刪除:x =

      這個字符串將被刪除:?=

      這個字符串將被刪除:


    現在,請大家自己試試運行結果,或許會更加慘不忍睹呢!下面,我為大家一一分析原因。

    首先,大家要知道,C++類有以下這些極為重要的函數:

    一:復制構造函數。

    二:賦值函數。

    我們先來講復制構造函數。什么是復制構造函數呢?比如,我們可以寫下這樣的代碼:String test1(test2);這是進行初始化。我們知道,初始化對象要用構造函數。可這兒呢?按理說,應該有聲明為這樣的構造函 數:String(const String &);可是,我們并沒有定義這個構造函數呀?答案是,C++提供了默認的復制構造函數,問題也就出在這兒。

    (1):什么時候會調用復制構造函數呢?(以String類為例。)

      在我們提供這樣的代碼:String test1(test2)時,它會被調用;當函數的參數列表為按值傳遞,也就是沒有用引用和指針作為類型時,如:void show_String(const String),它會被調用。其實,還有一些情況,但在這兒就不列舉了。

    (2):它是什么樣的函數。

    它的作用就是把兩個類進行復制。拿String類為例,C++提供的默認復制構造函數是這樣的:

  • String(const String& a)
  • {
  • str=a.str;
  • len=a.len;
  • }
  • 復制代碼

    在 平時,這樣并不會有任何的問題出現,但我們用了new操作符,涉及到了動態內存分配,我們就不得不談談淺復制和深復制了。以上的函數就是實行的淺復制,它 只是復制了指針,而并沒有復制指針指向的數據,可謂一點兒用也沒有。打個比方吧!就像一個朋友讓你把一個程序通過網絡發給他,而你大大咧咧地把快捷方式發 給了他,有什么用處呢?我們來具體談談:

    假如,A對象中存儲了這樣的字符串:“C++”。它的地址為2000。現在,我們把A對象賦給B對象:String B=A。現在,A和B對象的str指針均指向2000地址。看似可以使用,但如果B對象的析構函數被調用時,則地址2000處的字符串“C++”已經被從 內存中抹去,而A對象仍然指向地址2000。這時,如果我們寫下這樣的代碼:cout<<A<<endl;或是等待程序結束,A 對象的析構函數被調用時,A對象的數據能否顯示出來呢?只會是亂碼。而且,程序還會這樣做:連續對地址2000處使用兩次delete操作符,這樣的后果 是十分嚴重的!

    本例中,有這樣的代碼:

  • String* String1=new String(test1);
  • cout<<*String1<<endl;
  • delete String1;
  • 復制代碼

    假 設test1中str指向的地址為2000,而String中str指針同樣指向地址2000,我們刪除了2000處的數據,而test1對象呢?已經被 破壞了。大家從運行結果上可以看到,我們使用cout<<test1時,一點反應也沒有。而在test1的析構函數被調用時,顯示是這樣: “這個字符串將被刪除:”。

    再看看這段代碼:

  • cout<<"使用錯誤的函數:"<<endl;
  • show_String(test2);
  • cout<<test2<<endl;//這一段代碼出現嚴重的錯誤!
  • 復制代碼

    show_String 函數的參數列表void show_String(const String a)是按值傳遞的,所以,我們相當于執行了這樣的代碼:String a=test2;函數執行完畢,由于生存周期的緣故,對象a被析構函數刪除,我們馬上就可以看到錯誤的顯示結果了:這個字符串將被刪除:?=。當 然,test2也被破壞了。解決的辦法很簡單,當然是手工定義一個復制構造函數嘍!人力可以勝天!

  • String::String(const String& a)
  • {
  • len=a.len;
  • str=new char(len+1);
  • strcpy(str,a.str);
  • }
  • 復制代碼

    我 們執行的是深復制。這個函數的功能是這樣的:假設對象A中的str指針指向地址2000,內容為“I am a C++ Boy!”。我們執行代碼String B=A時,我們先開辟出一塊內存,假設為3000。我們用strcpy函數將地址2000的內容拷貝到地址3000中,再將對象B的str指針指向地址 3000。這樣,就互不干擾了。

    大家把這個函數加入程序中,問題就解決了大半,但還沒有完全解決,問題在賦值函數上。我們的程序中有這樣的段代碼:

  • String String3;
  • String3=test4;
  • 復制代碼

    經 過我前面的講解,大家應該也會對這段代碼進行尋根摸底:憑什么可以這樣做:String3=test4???原因是,C++為了用戶的方便,提供的這樣的 一個操作符重載函數:operator=。所以,我們可以這樣做。大家應該猜得到,它同樣是執行了淺復制,出了同樣的毛病。比如,執行了這段代碼后,析構 函數開始大展神威^_^。由于這些變量是后進先出的,所以最后的String3變量先被刪除:這個字符串將被刪除:第四個范例。很正常。最后,刪除到 test4的時候,問題來了:這個字符串將被刪除:?=。原因我不用贅述了,只是這個賦值函數怎么寫,還有一點兒學問呢!大家請看:

    平時,我們可以寫這樣的代碼:x=y=z。(均為整型變量。)而在類對象中,我們同樣要這樣,因為這很方便。而對象A=B=C就是 A.operator=(B.operator=(c))。而這個operator=函數的參數列表應該是:const String& a,所以,大家不難推出,要實現這樣的功能,返回值也要是String&,這樣才能實現A=B=C。我們先來寫寫看:

  • String& String::operator=(const String& a)
  • {
  • delete [] str;//先刪除自身的數據
  • len=a.len;
  • str=new char[len+1];
  • strcpy(str,a.str);//此三行為進行拷貝
  • return *this;//返回自身的引用
  • }
  • 復制代碼

    是 不是這樣就行了呢?我們假如寫出了這種代碼:A=A,那么大家看看,豈不是把A對象的數據給刪除了嗎?這樣可謂引發一系列的錯誤。所以,我們還要檢查是否 為自身賦值。只比較兩對象的數據是不行了,因為兩個對象的數據很有可能相同。我們應該比較地址。以下是完好的賦值函數:

  • String& String::operator=(const String& a)
  • {
  • if(this==&a)
  • return *this;
  • delete [] str;
  • len=a.len;
  • str=new char[len+1];
  • strcpy(str,a.str);
  • return *this;
  • }
  • 復制代碼

    把這些代碼加入程序,問題就完全解決,下面是運行結果:

      下面分別輸入三個范例:

      第一個范例

      第二個范例

      第三個范例

      第一個范例

      這個字符串將被刪除:第一個范例。

      第一個范例

       使用正確的函數:

      第二個范例。

      第二個范例。

       使用錯誤的函數:

      第二個范例。

      這個字符串將被刪除:第二個范例。

      第二個范例。

      String2: 第三個范例。

      String3: 第四個范例。

      下面,程序結束,析構函數將被調用。

      這個字符串將被刪除:第四個范例。

      這個字符串將被刪除:第三個范例。

      這個字符串將被刪除:第四個范例。

      這個字符串將被刪除:第三個范例。

      這個字符串將被刪除:第二個范例。

      這個字符串將被刪除:第一個范例。


    2.2 如何對付內存泄漏?

    寫出那些不會導致任何內存泄漏的代碼。很明顯,當你的代碼中到處充滿了new 操作、delete操作和指針運算的話,你將會在某個地方搞暈了頭,導致內存泄漏,指針引用錯誤,以及諸如此類的問題。這和你如何小心地對待內存分配工作 其實完全沒有關系:代碼的復雜性最終總是會超過你能夠付出的時間和努力。于是隨后產生了一些成功的技巧,它們依賴于將內存分配(allocations) 與重新分配(deallocation)工作隱藏在易于管理的類型之后。標準容器(standard containers)是一個優秀的例子。它們不是通過你而是自己為元素管理內存,從而避免了產生糟糕的結果。想象一下,沒有string和vector 的幫助,寫出這個:

  • #include<vector>
  • #include<string>
  • #include<iostream>
  • #include<algorithm>
  • using namespace std;
  • int main() // small program messing around with strings
  • {
  •  cout << "enter some whitespace-separated words:"n";
  •  vector<string> v;
  •  string s;
  •  while (cin>>s) v.push_back(s);
  •  sort(v.begin(),v.end());
  •  string cat;
  •  typedef vector<string>::const_iterator Iter;
  •  for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";
  •  cout << cat << ’"n’;
  • }
  • 復制代碼

    你有多少機會在第一次就得到正確的結果?你又怎么知道你沒有導致內存泄漏呢?

      注意,沒有出現顯式的內存管理,宏,造型,溢出檢查,顯式的長度限制,以及指針。通過使用函數對象和標準算法(standard algorithm),我可以避免使用指針——例如使用迭代子(iterator),不過對于一個這么小的程序來說有點小題大作了。

      這些技巧并不完美,要系統化地使用它們也并不總是那么容易。但是,應用它們產生了驚人的差異,而且通過減少顯式的內存分配與重新分配的次數,你甚至可 以使余下的例子更加容易被跟蹤。早在1981年,我就指出,通過將我必須顯式地跟蹤的對象的數量從幾萬個減少到幾打,為了使程序正確運行而付出的努力從可 怕的苦工,變成了應付一些可管理的對象,甚至更加簡單了。

      如果你的程序還沒有包含將顯式內存管理減少到最小限度的庫,那么要讓你程序完成和正確運行的話,最快的途徑也許就是先建立一個這樣的庫。

      模板和標準庫實現了容器、資源句柄以及諸如此類的東西,更早的使用甚至在多年以前。異常的使用使之更加完善。

      如果你實在不能將內存分配/重新分配的操作隱藏到你需要的對象中時,你可以使用資源句柄(resource handle),以將內存泄漏的可能性降至最低。這里有個例子:我需要通過一個函數,在空閑內存中建立一個對象并返回它。這時候可能忘記釋放這個對象。畢 竟,我們不能說,僅僅關注當這個指針要被釋放的時候,誰將負責去做。使用資源句柄,這里用了標準庫中的auto_ptr,使需要為之負責的地方變得明確 了。

  • #include<memory>
  • #include<iostream>
  • using namespace std;
  • struct S {
  •  S() { cout << "make an S"n"; }
  •  ~S() { cout << "destroy an S"n"; }
  •  S(const S&) { cout << "copy initialize an S"n"; }
  •  S& operator=(const S&) { cout << "copy assign an S"n"; }
  • };
  • S* f()
  • {
  •  return new S; // 誰該負責釋放這個S?
  • };
  • auto_ptr<S> g()
  • {
  •  return auto_ptr<S>(new S); // 顯式傳遞負責釋放這個S
  • }
  • int main()
  • {
  •  cout << "start main"n";
  •  S* p = f();
  •  cout << "after f() before g()"n";
  •  // S* q = g(); // 將被編譯器捕捉
  •  auto_ptr<S> q = g();
  •  cout << "exit main"n";
  •  // *p產生了內存泄漏
  •  // *q被自動釋放
  • }
  • 復制代碼

    在更一般的意義上考慮資源,而不僅僅是內存。

    如果在你的環境中不能系統地應用這些技巧(例如,你必須使用別的地方的代碼,或者你的程序的另一部分簡直是原始人類(譯注:原文是 Neanderthals,尼安德特人,舊石器時代廣泛分布在歐洲的猿人)寫的,如此等等),那么注意使用一個內存泄漏檢測器作為開發過程的一部分,或者 插入一個垃圾收集器(garbage collector)。

    2.3淺談C/C++內存泄漏及其檢測工具

      對于一個c/c++程序員來說,內存泄漏是一個常見的也是令人頭疼的問題。已經有許多技術被研究出來以應對這個問題,比如Smart Pointer,Garbage Collection等。Smart Pointer技術比較成熟,STL中已經包含支持Smart Pointer的class,但是它的使用似乎并不廣泛,而且它也不能解決所有的問題;Garbage Collection技術在Java中已經比較成熟,但是在c/c++領域的發展并不順暢,雖然很早就有人思考在C++中也加入GC的支持。現實世界就是 這樣的,作為一個c/c++程序員,內存泄漏是你心中永遠的痛。不過好在現在有許多工具能夠幫助我們驗證內存泄漏的存在,找出發生問題的代碼。

    2.3.1 內存泄漏的定義

    一般我們常說的內存泄漏是指堆內存的泄漏。堆內存是指程序從堆中分配的,大小任意的(內存塊的大小可以在程序運行期決定),使用完后必須顯示釋放的內存。 應用程序一般使用malloc,realloc,new等函數從堆中分配到一塊內存,使用完后,程序必須負責相應的調用free或delete釋放該內存 塊,否則,這塊內存就不能被再次使用,我們就說這塊內存泄漏了。以下這段小程序演示了堆內存發生泄漏的情形:

  • void MyFunction(int nSize)
  • {
  •  char* p= new char[nSize];
  •  if( !GetStringFrom( p, nSize ) ){
  •   MessageBox(“Error”);
  •   return;
  •  }
  •  …//using the string pointed by p;
  •  delete p;
  • }
  • 復制代碼

    當函數GetStringFrom()返回零的時候,指針p指向的內存就不會被釋放。這是一種常見的發生內存泄漏的情形。程序在入口處分配內存,在出口處釋放內存,但是c函數可以在任何地方退出,所以一旦有某個出口處沒有釋放應該釋放的內存,就會發生內存泄漏。

      廣義的說,內存泄漏不僅僅包含堆內存的泄漏,還包含系統資源的泄漏(resource leak),比如核心態HANDLE,GDI Object,SOCKET, Interface等,從根本上說這些由操作系統分配的對象也消耗內存,如果這些對象發生泄漏最終也會導致內存的泄漏。而且,某些對象消耗的是核心態內 存,這些對象嚴重泄漏時會導致整個操作系統不穩定。所以相比之下,系統資源的泄漏比堆內存的泄漏更為嚴重。

    GDI Object的泄漏是一種常見的資源泄漏:

  • void CMyView::OnPaint( CDC* pDC )
  • {
  •  CBitmap bmp;
  •  CBitmap* pOldBmp;
  •  bmp.LoadBitmap(IDB_MYBMP);
  •  pOldBmp = pDC->SelectObject( &bmp );
  •  …
  •  if( Something() ){
  •   return;
  •  }
  •  pDC->SelectObject( pOldBmp );
  •  return;
  • }
  • 復制代碼

    當 函數Something()返回非零的時候,程序在退出前沒有把pOldBmp選回pDC中,這會導致pOldBmp指向的HBITMAP對象發生泄漏。 這個程序如果長時間的運行,可能會導致整個系統花屏。這種問題在Win9x下比較容易暴露出來,因為Win9x的GDI堆比Win2k或NT 的要小很多。

    2.3.2 內存泄漏的發生方式

      以發生的方式來分類,內存泄漏可以分為4類:

      1. 常發性內存泄漏。發生內存泄漏的代碼會被多次執行到,每次被執行的時候都會導致一塊內存泄漏。比如例二,如果Something()函數一直返回True,那么pOldBmp指向的HBITMAP對象總是發生泄漏。

      2. 偶發性內存泄漏。發生內存泄漏的代碼只有在某些特定環境或操作過程下才會發生。比如例二,如果Something()函數只有在特定環境下才返回 True,那么pOldBmp指向的HBITMAP對象并不總是發生泄漏。常發性和偶發性是相對的。對于特定的環境,偶發性的也許就變成了常發性的。所以 測試環境和測試方法對檢測內存泄漏至關重要。

    3. 一次性內存泄漏。發生內存泄漏的代碼只會被執行一次,或者由于算法上的缺陷,導致總會有一塊僅且一塊內存發生泄漏。比如,在類的構造函數中分配內存,在析構函數中卻沒有釋放該內存,但是因為這個類是一個Singleton,所以內存泄漏只會發生一次。另一個例子:

  • char* g_lpszFileName = NULL;
  • void SetFileName( const char* lpcszFileName )
  • {
  •  if( g_lpszFileName ){
  •   free( g_lpszFileName );
  •  }
  •  g_lpszFileName = strdup( lpcszFileName );
  • }
  • 復制代碼

    如果程序在結束的時候沒有釋放g_lpszFileName指向的字符串,那么,即使多次調用SetFileName(),總會有一塊內存,而且僅有一塊內存發生泄漏。

    4. 隱式內存泄漏。程序在運行過程中不停的分配內存,但是直到結束的時候才釋放內存。嚴格的說這里并沒有發生內存泄漏,因為最終程序釋放了所有申請的內存。但 是對于一個服務器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內存也可能導致最終耗盡系統的所有內存。所以,我們稱這類內存泄漏為隱式內存泄漏。舉一 個例子:

  • class Connection
  • {
  •  public:
  •   Connection( SOCKET s);
  •   ~Connection();
  •   …
  •  private:
  •   SOCKET _socket;
  •   …
  • };
  • class ConnectionManager
  • {
  •  public:
  •   ConnectionManager(){}
  •   ~ConnectionManager(){
  •    list::iterator it;
  •    for( it = _connlist.begin(); it != _connlist.end(); ++it ){
  •     delete (*it);
  •    }
  •    _connlist.clear();
  •   }
  •   void OnClientConnected( SOCKET s ){
  •    Connection* p = new Connection(s);
  •    _connlist.push_back(p);
  •   }
  •   void OnClientDisconnected( Connection* pconn ){
  •    _connlist.remove( pconn );
  •    delete pconn;
  •   }
  •  private:
  •   list _connlist;
  • };
  • 復制代碼

    假 設在Client從Server端斷開后,Server并沒有呼叫OnClientDisconnected()函數,那么代表那次連接的 Connection對象就不會被及時的刪除(在Server程序退出的時候,所有Connection對象會在ConnectionManager的析 構函數里被刪除)。當不斷的有連接建立、斷開時隱式內存泄漏就發生了。

    從用戶使用程序的角度來看,內存泄漏本身不會產生什么危害,作為一般的用戶,根本感覺不到內存泄漏的存在。真正有危害的是內存泄漏的堆積,這會最終消耗盡 系統所有的內存。從這個角度來說,一次性內存泄漏并沒有什么危害,因為它不會堆積,而隱式內存泄漏危害性則非常大,因為較之于常發性和偶發性內存泄漏它更 難被檢測到。

    2.3.3 檢測內存泄漏

      檢測內存泄漏的關鍵是要能截獲住對分配內存和釋放內存的函數的調用。截獲住這兩個函數,我們就能跟蹤每一塊內存的生命周期,比如,每當成功的分配一塊 內存后,就把它的指針加入一個全局的list中;每當釋放一塊內存,再把它的指針從list中刪除。這樣,當程序結束的時候,list中剩余的指針就是指 向那些沒有被釋放的內存。這里只是簡單的描述了檢測內存泄漏的基本原理,詳細的算法可以參見Steve Maguire的<<Writing Solid Code>>。

      如果要檢測堆內存的泄漏,那么需要截獲住malloc/realloc/free和new/delete就可以了(其實new/delete最終也是 用malloc/free的,所以只要截獲前面一組即可)。對于其他的泄漏,可以采用類似的方法,截獲住相應的分配和釋放函數。比如,要檢測 BSTR的泄漏,就需要截獲SysAllocString/SysFreeString;要檢測HMENU的泄漏,就需要截獲CreateMenu/ DestroyMenu。(有的資源的分配函數有多個,釋放函數只有一個,比如,SysAllocStringLen也可以用來分配BSTR,這時就需要 截獲多個分配函數)

      在Windows平臺下,檢測內存泄漏的工具常用的一般有三種,MS C-Runtime Library內建的檢測功能;外掛式的檢測工具,諸如,Purify,BoundsChecker等;利用Windows NT自帶的Performance Monitor。這三種工具各有優缺點,MS C-Runtime Library雖然功能上較之外掛式的工具要弱,但是它是免費的;Performance Monitor雖然無法標示出發生問題的代碼,但是它能檢測出隱式的內存泄漏的存在,這是其他兩類工具無能為力的地方。

      以下我們詳細討論這三種檢測工具:

    2.3.3.1 VC下內存泄漏的檢測方法

      用MFC開發的應用程序,在DEBUG版模式下編譯后,都會自動加入內存泄漏的檢測代碼。在程序結束后,如果發生了內存泄漏,在Debug窗口中會顯示出所有發生泄漏的內存塊的信息,以下兩行顯示了一塊被泄漏的內存塊的信息:

    E:"TestMemLeak"TestDlg.cpp(70) : {59} normal block at 0x00881710, 200 bytes long.

    Data: <abcdefghijklmnop> 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

      第一行顯示該內存塊由TestDlg.cpp文件,第70行代碼分配,地址在0x00881710,大小為200字節,{59}是指調用內存分配函數 的Request Order,關于它的詳細信息可以參見MSDN中_CrtSetBreakAlloc()的幫助。第二行顯示該內存塊前16個字節的內容,尖括號內是以 ASCII方式顯示,接著的是以16進制方式顯示。

      一般大家都誤以為這些內存泄漏的檢測功能是由MFC提供的,其實不然。MFC只是封裝和利用了MS C-Runtime Library的Debug Function。非MFC程序也可以利用MS C-Runtime Library的Debug Function加入內存泄漏的檢測功能。MS C-Runtime Library在實現malloc/free,strdup等函數時已經內建了內存泄漏的檢測功能。

    注意觀察一下由MFC Application Wizard生成的項目,在每一個cpp文件的頭部都有這樣一段宏定義:

  • #ifdef _DEBUG
  • #define new DEBUG_NEW
  • #undef THIS_FILE
  • static char THIS_FILE[] = __FILE__;
  • #endif
  • 復制代碼

    有了這樣的定義,在編譯DEBUG版時,出現在這個cpp文件中的所有new都被替換成DEBUG_NEW了。那么DEBUG_NEW是什么呢?DEBUG_NEW也是一個宏,以下摘自afx.h,1632行

  • #define DEBUG_NEW new(THIS_FILE, __LINE__)
  • 復制代碼

    所以如果有這樣一行代碼:

  • char* p = new char[200];
  • 復制代碼

    經過宏替換就變成了:

  • char* p = new( THIS_FILE, __LINE__)char[200];
  • 復制代碼

    根據C++的標準,對于以上的new的使用方法,編譯器會去找這樣定義的operator new:

  • void* operator new(size_t, LPCSTR, int)
  • 復制代碼

    我們在afxmem.cpp 63行找到了一個這樣的operator new 的實現

  • void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine)
  • {
  •  return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);
  • }
  • void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)
  • {
  •  …
  •  pResult = _malloc_dbg(nSize, nType, lpszFileName, nLine);
  •  if (pResult != NULL)
  •   return pResult;
  •  …
  • }
  • 復制代碼

    第 二個operator new函數比較長,為了簡單期間,我只摘錄了部分。很顯然最后的內存分配還是通過_malloc_dbg函數實現的,這個函數屬于MS C-Runtime Library 的Debug Function。這個函數不但要求傳入內存的大小,另外還有文件名和行號兩個參數。文件名和行號就是用來記錄此次分配是由哪一段代碼造成的。如果這塊內 存在程序結束之前沒有被釋放,那么這些信息就會輸出到Debug窗口里。

      這里順便提一下THIS_FILE,__FILE和__LINE__。__FILE__和__LINE__都是編譯器定義的宏。當碰到 __FILE__時,編譯器會把__FILE__替換成一個字符串,這個字符串就是當前在編譯的文件的路徑名。當碰到__LINE__時,編譯器會把 __LINE__替換成一個數字,這個數字就是當前這行代碼的行號。在DEBUG_NEW的定義中沒有直接使用__FILE__,而是用了 THIS_FILE,其目的是為了減小目標文件的大小。假設在某個cpp文件中有100處使用了new,如果直接使用__FILE__,那編譯器會產生 100個常量字符串,這100個字符串都是飧?/SPAN>cpp文件的路徑名,顯然十分冗余。如果使用THIS_FILE,編譯器只會產生一個常 量字符串,那100處new的調用使用的都是指向常量字符串的指針。

      再次觀察一下由MFC Application Wizard生成的項目,我們會發現在cpp文件中只對new做了映射,如果你在程序中直接使用malloc函數分配內存,調用malloc的文件名和行 號是不會被記錄下來的。如果這塊內存發生了泄漏,MS C-Runtime Library仍然能檢測到,但是當輸出這塊內存塊的信息,不會包含分配它的的文件名和行號。

    要在非MFC程序中打開內存泄漏的檢測功能非常容易,你只要在程序的入口處加入以下幾行代碼:

  • int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );
  • tmpFlag |= _CRTDBG_LEAK_CHECK_DF;
  • _CrtSetDbgFlag( tmpFlag );
  • 復制代碼

    這樣,在程序結束的時候,也就是winmain,main或dllmain函數返回之后,如果還有內存塊沒有釋放,它們的信息會被打印到Debug窗口里。

    如果你試著創建了一個非MFC應用程序,而且在程序的入口處加入了以上代碼,并且故意在程序中不釋放某些內存塊,你會在Debug窗口里看到以下的信息:

    {47} normal block at 0x00C91C90, 200 bytes long.

    Data: < > 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F


      內存泄漏的確檢測到了,但是和上面MFC程序的例子相比,缺少了文件名和行號。對于一個比較大的程序,沒有這些信息,解決問題將變得十分困難。

      為了能夠知道泄漏的內存塊是在哪里分配的,你需要實現類似MFC的映射功能,把new,maolloc等函數映射到_malloc_dbg函數上。這里我不再贅述,你可以參考MFC的源代碼。

      由于Debug Function實現在MS C-RuntimeLibrary中,所以它只能檢測到堆內存的泄漏,而且只限于malloc,realloc或strdup等分配的內存,而那些系統資 源,比如HANDLE,GDI Object,或是不通過C-Runtime Library分配的內存,比如VARIANT,BSTR的泄漏,它是無法檢測到的,這是這種檢測法的一個重大的局限性。另外,為了能記錄內存塊是在哪里 分配的,源代碼必須相應的配合,這在調試一些老的程序非常麻煩,畢竟修改源代碼不是一件省心的事,這是這種檢測法的另一個局限性。

    對于開發一個大型的程序,MS C-Runtime Library提供的檢測功能是遠遠不夠的。接下來我們就看看外掛式的檢測工具。我用的比較多的是BoundsChecker,一則因為它的功能比較全 面,更重要的是它的穩定性。這類工具如果不穩定,反而會忙里添亂。到底是出自鼎鼎大名的NuMega,我用下來基本上沒有什么大問題。

    2.3.3.2 使用BoundsChecker檢測內存泄漏

      BoundsChecker采用一種被稱為 Code Injection的技術,來截獲對分配內存和釋放內存的函數的調用。簡單地說,當你的程序開始運行時,BoundsChecker的DLL被自動載入進 程的地址空間(這可以通過system-level的Hook實現),然后它會修改進程中對內存分配和釋放的函數調用,讓這些調用首先轉入它的代碼,然后 再執行原來的代碼。BoundsChecker在做這些動作的時,無須修改被調試程序的源代碼或工程配置文件,這使得使用它非常的簡便、直接。

      這里我們以malloc函數為例,截獲其他的函數方法與此類似。

      需要被截獲的函數可能在DLL中,也可能在程序的代碼里。比如,如果靜態連結C-Runtime Library,那么malloc函數的代碼會被連結到程序里。為了截獲住對這類函數的調用,BoundsChecker會動態修改這些函數的指令。

    以下兩段匯編代碼,一段沒有BoundsChecker介入,另一段則有BoundsChecker的介入:

    126: _CRTIMP void * __cdecl malloc (

    127: size_t nSize

    128: )

    129: {

    00403C10 push ebp

    00403C11 mov ebp,esp

    130: return _nh_malloc_dbg(nSize, _newmode, _NORMAL_BLOCK, NULL, 0);

    00403C13 push 0

    00403C15 push 0

    00403C17 push 1

    00403C19 mov eax,[__newmode (0042376c)]

    00403C1E push eax

    00403C1F mov ecx,dword ptr [nSize]

    00403C22 push ecx

    00403C23 call _nh_malloc_dbg (00403c80)

    00403C28 add esp,14h

    131: }


    以下這一段代碼有BoundsChecker介入:

    126: _CRTIMP void * __cdecl malloc (

    127: size_t nSize

    128: )

    129: {

    00403C10 jmp 01F41EC8

    00403C15 push 0

    00403C17 push 1

    00403C19 mov eax,[__newmode (0042376c)]

    00403C1E push eax

    00403C1F mov ecx,dword ptr [nSize]

    00403C22 push ecx

    00403C23 call _nh_malloc_dbg (00403c80)

    00403C28 add esp,14h

    131: }


      當BoundsChecker介入后,函數malloc的前三條匯編指令被替換成一條jmp指令,原來的三條指令被搬到地址01F41EC8處了。當 程序進入malloc后先jmp到01F41EC8,執行原來的三條指令,然后就是BoundsChecker的天下了。大致上它會先記錄函數的返回地址 (函數的返回地址在stack上,所以很容易修改),然后把返回地址指向屬于BoundsChecker的代碼,接著跳到malloc函數原來的指令,也 就是在00403c15的地方。當malloc函數結束的時候,由于返回地址被修改,它會返回到BoundsChecker的代碼中,此時 BoundsChecker會記錄由malloc分配的內存的指針,然后再跳轉到到原來的返回地址去。

      如果內存分配/釋放函數在DLL中,BoundsChecker則采用另一種方法來截獲對這些函數的調用。BoundsChecker通過修改程序的DLL Import Table讓table中的函數地址指向自己的地址,以達到截獲的目的。

    截獲住這些分配和釋放函數,BoundsChecker就能記錄被分配的內存或資源的生命周期。接下來的問題是如何與源代碼相關,也就是說當 BoundsChecker檢測到內存泄漏,它如何報告這塊內存塊是哪段代碼分配的。答案是調試信息(Debug Information)。當我們編譯一個Debug版的程序時,編譯器會把源代碼和二進制代碼之間的對應關系記錄下來,放到一個單獨的文件里 (.pdb)或者直接連結進目標程序,通過直接讀取調試信息就能得到分配某塊內存的源代碼在哪個文件,哪一行上。使用Code Injection和Debug Information,使BoundsChecker不但能記錄呼叫分配函數的源代碼的位置,而且還能記錄分配時的Call Stack,以及Call Stack上的函數的源代碼位置。這在使用像MFC這樣的類庫時非常有用,以下我用一個例子來說明:

    void ShowXItemMenu()
    {
     …
     CMenu menu;
     menu.CreatePopupMenu();
     //add menu items.
     menu.TrackPropupMenu();
     …
    }

    void ShowYItemMenu( )
    {
     …
     CMenu menu;
     menu.CreatePopupMenu();
     //add menu items.
     menu.TrackPropupMenu();
     menu.Detach();//this will cause HMENU leak
     …
    }

    BOOL CMenu::CreatePopupMenu()
    {
     …
     hMenu = CreatePopupMenu();
     …
    }

    當調用ShowYItemMenu()時,我們故意造成HMENU的泄漏。但是,對于BoundsChecker來說被泄漏的HMENU是在 class CMenu::CreatePopupMenu()中分配的。假設的你的程序有許多地方使用了CMenu的CreatePopupMenu()函數,如 CMenu::CreatePopupMenu()造成的,你依然無法確認問題的根結到底在哪里,在ShowXItemMenu()中還是在 ShowYItemMenu()中,或者還有其它的地方也使用了CreatePopupMenu()?有了Call Stack的信息,問題就容易了。BoundsChecker會如下報告泄漏的HMENU的信息:

    Function

    File

    Line

    CMenu::CreatePopupMenu

    E:"8168"vc98"mfc"mfc"include"afxwin1.inl

    1009

    ShowYItemMenu

    E:"testmemleak"mytest.cpp

    100


      這里省略了其他的函數調用

      如此,我們很容易找到發生問題的函數是ShowYItemMenu()。當使用MFC之類的類庫編程時,大部分的API調用都被封裝在類庫的class里,有了Call Stack信息,我們就可以非常容易的追蹤到真正發生泄漏的代碼。

      記錄Call Stack信息會使程序的運行變得非常慢,因此默認情況下BoundsChecker不會記錄Call Stack信息。可以按照以下的步驟打開記錄Call Stack信息的選項開關:

      1. 打開菜單:BoundsChecker|Setting…

      2. 在Error Detection頁中,在Error Detection Scheme的List中選擇Custom

      3. 在Category的Combox中選擇 Pointer and leak error check

      4. 鉤上Report Call Stack復選框

      5. 點擊Ok

      基于Code Injection,BoundsChecker還提供了API Parameter的校驗功能,memory over run等功能。這些功能對于程序的開發都非常有益。由于這些內容不屬于本文的主題,所以不在此詳述了。

    盡管BoundsChecker的功能如此強大,但是面對隱式內存泄漏仍然顯得蒼白無力。所以接下來我們看看如何用Performance Monitor檢測內存泄漏。

    2.3.3.3 使用Performance Monitor檢測內存泄漏
      NT的內核在設計過程中已經加入了系統監視功能,比如CPU 的使用率,內存的使用情況,I/O操作的頻繁度等都作為一個個Counter,應用程序可以通過讀取這些Counter了解整個系統的或者某個進程的運行 狀況。Performance Monitor就是這樣一個應用程序。

      為了檢測內存泄漏,我們一般可以監視Process對象的Handle Count,Virutal Bytes 和Working Set三個Counter。Handle Count記錄了進程當前打開的HANDLE的個數,監視這個Counter有助于我們發現程序是否有Handle泄漏;Virtual Bytes記錄了該進程當前在虛地址空間上使用的虛擬內存的大小,NT的內存分配采用了兩步走的方法,首先,在虛地址空間上保留一段空間,這時操作系統并 沒有分配物理內存,只是保留了一段地址。然后,再提交這段空間,這時操作系統才會分配物理內存。所以,Virtual Bytes一般總大于程序的Working Set。監視Virutal Bytes可以幫助我們發現一些系統底層的問題; Working Set記錄了操作系統為進程已提交的內存的總量,這個值和程序申請的內存總量存在密切的關系,如果程序存在內存的泄漏這個值會持續增加,但是 Virtual Bytes卻是跳躍式增加的。

      監視這些Counter可以讓我們了解進程使用內存的情況,如果發生了泄漏,即使是隱式內存泄漏,這些Counter的值也會持續增加。但是,我們知 道有問題卻不知道哪里有問題,所以一般使用Performance Monitor來驗證是否有內存泄漏,而使用BoundsChecker來找到和解決。

      當Performance Monitor顯示有內存泄漏,而BoundsChecker卻無法檢測到,這時有兩種可能:第一種,發生了偶發性內存泄漏。這時你要確保使用 Performance Monitor和使用BoundsChecker時,程序的運行環境和操作方法是一致的。第二種,發生了隱式的內存泄漏。這時你要重新審查程序的設計,然 后仔細研究Performance Monitor記錄的Counter的值的變化圖,分析其中的變化和程序運行邏輯的關系,找到一些可能的原因。這是一個痛苦的過程,充滿了假設、猜想、驗 證、失敗,但這也是一個積累經驗的絕好機會。

    3 探討C++內存回收

    3.1 C++內存對象大會戰

      如果一個人自稱為程序高手,卻對內存一無所知,那么我可以告訴你,他一定在吹牛。用C或C++寫程序,需要更多地關注內存,這不僅僅是因為內存的分配 是否合理直接影響著程序的效率和性能,更為主要的是,當我們操作內存的時候一不小心就會出現問題,而且很多時候,這些問題都是不易發覺的,比如內存泄漏, 比如懸掛指針。筆者今天在這里并不是要討論如何避免這些問題,而是想從另外一個角度來認識C++內存對象。

      我們知道,C++將內存劃分為三個邏輯區域:堆、棧和靜態存儲區。既然如此,我稱位于它們之中的對象分別為堆對象,棧對象以及靜態對象。那么這些不同的內存對象有什么區別了?堆對象和棧對象各有什么優劣了?如何禁止創建堆對象或棧對象了?這些便是今天的主題。

    3.1.1 基本概念

      先來看看棧。棧,一般用于存放局部變量或對象,如我們在函數定義中用類似下面語句聲明的對象:

  • Type stack_object;
  • 復制代碼

    stack_object便是一個棧對象,它的生命期是從定義點開始,當所在函數返回時,生命結束。

      另外,幾乎所有的臨時對象都是棧對象。比如,下面的函數定義:

  • Type fun(Type object);
  • 復制代碼

    這 個函數至少產生兩個臨時對象,首先,參數是按值傳遞的,所以會調用拷貝構造函數生成一個臨時對象object_copy1 ,在函數內部使用的不是使用的不是object,而是object_copy1,自然,object_copy1是一個棧對象,它在函數返回時被釋放;還 有這個函數是值返回的,在函數返回時,如果我們不考慮返回值優化(NRV),那么也會產生一個臨時對象object_copy2,這個臨時對象會在函數返 回后一段時間內被釋放。比如某個函數中有如下代碼:

  • Type tt ,result ; //生成兩個棧對象
  • tt = fun(tt); //函數返回時,生成的是一個臨時對象object_copy2
  • 復制代碼

    上面的第二個語句的執行情況是這樣的,首先函數fun返回時生成一個臨時對象object_copy2 ,然后再調用賦值運算符執行

  • tt = object_copy2 ; //調用賦值運算符
  • 復制代碼

    看到了嗎?編譯器在我們毫無知覺的情況下,為我們生成了這么多臨時對象,而生成這些臨時對象的時間和空間的開銷可能是很大的,所以,你也許明白了,為什么對于“大”對象最好用const引用傳遞代替按值進行函數參數傳遞了。

      接下來,看看堆。堆,又叫自由存儲區,它是在程序執行的過程中動態分配的,所以它最大的特性就是動態性。在C++中,所有堆對象的創建和銷毀都要由程 序員負責,所以,如果處理不好,就會發生內存問題。如果分配了堆對象,卻忘記了釋放,就會產生內存泄漏;而如果已釋放了對象,卻沒有將相應的指針置為 NULL,該指針就是所謂的“懸掛指針”,再度使用此指針時,就會出現非法訪問,嚴重時就導致程序崩潰。

      那么,C++中是怎樣分配堆對象的?唯一的方法就是用new(當然,用類malloc指令也可獲得C式堆內存),只要使用new,就會在堆中分配一塊內存,并且返回指向該堆對象的指針。

      再來看看靜態存儲區。所有的靜態對象、全局對象都于靜態存儲區分配。關于全局對象,是在main()函數執行前就分配好了的。其實,在 main()函數中的顯示代碼執行之前,會調用一個由編譯器生成的_main()函數,而_main()函數會進行所有全局對象的的構造及初始化工作。而 在main()函數結束之前,會調用由編譯器生成的exit函數,來釋放所有的全局對象。比如下面的代碼:

  • void main(void)
  • {
  •  … …// 顯式代碼
  • }
  • 復制代碼

    實際上,被轉化成這樣:

  • void main(void)
  • {
  •  _main(); //隱式代碼,由編譯器產生,用以構造所有全局對象
  •  … … // 顯式代碼
  •  exit() ; // 隱式代碼,由編譯器產生,用以釋放所有全局對象
  • }
  • 復制代碼

    所 以,知道了這個之后,便可以由此引出一些技巧,如,假設我們要在main()函數執行之前做某些準備工作,那么我們可以將這些準備工作寫到一個自定義的全 局對象的構造函數中,這樣,在main()函數的顯式代碼執行之前,這個全局對象的構造函數會被調用,執行預期的動作,這樣就達到了我們的目的。剛才講的 是靜態存儲區中的全局對象,那么,局部靜態對象了?局部靜態對象通常也是在函數中定義的,就像棧對象一樣,只不過,其前面多了個static關鍵字。局部 靜態對象的生命期是從其所在函數第一次被調用,更確切地說,是當第一次執行到該靜態對象的聲明代碼時,產生該靜態局部對象,直到整個程序結束時,才銷毀該 對象。

      還有一種靜態對象,那就是它作為class的靜態成員。考慮這種情況時,就牽涉了一些較復雜的問題。

      第一個問題是class的靜態成員對象的生命期,class的靜態成員對象隨著第一個class object的產生而產生,在整個程序結束時消亡。也就是有這樣的情況存在,在程序中我們定義了一個class,該類中有一個靜態對象作為成員,但是在程 序執行過程中,如果我們沒有創建任何一個該class object,那么也就不會產生該class所包含的那個靜態對象。還有,如果創建了多個class object,那么所有這些object都共享那個靜態對象成員。

      第二個問題是,當出現下列情況時:

  • class Base
  • {
  •  public:
  •   static Type s_object ;
  • }
  • class Derived1 : public Base / / 公共繼承
  • {
  •  … …// other data
  • }
  • class Derived2 : public Base / / 公共繼承
  • {
  •  … …// other data
  • }
  • Base example ;
  • Derivde1 example1 ;
  • Derivde2 example2 ;
  • example.s_object = …… ;
  • example1.s_object = …… ;
  • example2.s_object = …… ;
  • 復制代碼

    請 注意上面標為黑體的三條語句,它們所訪問的s_object是同一個對象嗎?答案是肯定的,它們的確是指向同一個對象,這聽起來不像是真的,是嗎?但這是 事實,你可以自己寫段簡單的代碼驗證一下。我要做的是來解釋為什么會這樣?我們知道,當一個類比如Derived1,從另一個類比如Base繼承時,那 么,可以看作一個Derived1對象中含有一個Base型的對象,這就是一個subobject。一個Derived1對象的大致內存布局如下:
      
      讓我們想想,當我們將一個Derived1型的對象傳給一個接受非引用Base型參數的函數時會發生切割,那么是怎么切割的呢?相信現在你已經知道 了,那就是僅僅取出了Derived1型的對象中的subobject,而忽略了所有Derived1自定義的其它數據成員,然后將這個 subobject傳遞給函數(實際上,函數中使用的是這個subobject的拷貝)。

      所有繼承Base類的派生類的對象都含有一個Base型的subobject(這是能用Base型指針指向一個Derived1對象的關鍵所在,自然 也是多態的關鍵了),而所有的subobject和所有Base型的對象都共用同一個s_object對象,自然,從Base類派生的整個繼承體系中的類 的實例都會共用同一個s_object對象了。上面提到的example、example1、example2的對象布局如下圖所示:

    3.1.2 三種內存對象的比較

      棧對象的優勢是在適當的時候自動生成,又在適當的時候自動銷毀,不需要程序員操心;而且棧對象的創建速度一般較堆對象快,因為分配堆對象時,會調用 operator new操作,operator new會采用某種內存空間搜索算法,而該搜索過程可能是很費時間的,產生棧對象則沒有這么麻煩,它僅僅需要移動棧頂指針就可以了。但是要注意的是,通常棧 空間容量比較小,一般是1MB~2MB,所以體積比較大的對象不適合在棧中分配。特別要注意遞歸函數中最好不要使用棧對象,因為隨著遞歸調用深度的增加, 所需的棧空間也會線性增加,當所需棧空間不夠時,便會導致棧溢出,這樣就會產生運行時錯誤。

      堆對象,其產生時刻和銷毀時刻都要程序員精確定義,也就是說,程序員對堆對象的生命具有完全的控制權。我們常常需要這樣的對象,比如,我們需要創建一 個對象,能夠被多個函數所訪問,但是又不想使其成為全局的,那么這個時候創建一個堆對象無疑是良好的選擇,然后在各個函數之間傳遞這個堆對象的指針,便可 以實現對該對象的共享。另外,相比于棧空間,堆的容量要大得多。實際上,當物理內存不夠時,如果這時還需要生成新的堆對象,通常不會產生運行時錯誤,而是 系統會使用虛擬內存來擴展實際的物理內存。

    接下來看看static對象。

      首先是全局對象。全局對象為類間通信和函數間通信提供了一種最簡單的方式,雖然這種方式并不優雅。一般而言,在完全的面向對象語言中,是不存在全局對 象的,比如C#,因為全局對象意味著不安全和高耦合,在程序中過多地使用全局對象將大大降低程序的健壯性、穩定性、可維護性和可復用性。C++也完全可以 剔除全局對象,但是最終沒有,我想原因之一是為了兼容C。

      其次是類的靜態成員,上面已經提到,基類及其派生類的所有對象都共享這個靜態成員對象,所以當需要在這些class之間或這些class objects之間進行數據共享或通信時,這樣的靜態成員無疑是很好的選擇。

      接著是靜態局部對象,主要可用于保存該對象所在函數被屢次調用期間的中間狀態,其中一個最顯著的例子就是遞歸函數,我們都知道遞歸函數是自己調用自己 的函數,如果在遞歸函數中定義一個nonstatic局部對象,那么當遞歸次數相當大時,所產生的開銷也是巨大的。這是因為nonstatic局部對象是 棧對象,每遞歸調用一次,就會產生一個這樣的對象,每返回一次,就會釋放這個對象,而且,這樣的對象只局限于當前調用層,對于更深入的嵌套層和更淺露的外 層,都是不可見的。每個層都有自己的局部對象和參數。

      在遞歸函數設計中,可以使用static對象替代nonstatic局部對象(即棧對象),這不僅可以減少每次遞歸調用和返回時產生和釋放nonstatic對象的開銷,而且static對象還可以保存遞歸調用的中間狀態,并且可為各個調用層所訪問。

    3.1.3 使用棧對象的意外收獲

      前面已經介紹到,棧對象是在適當的時候創建,然后在適當的時候自動釋放的,也就是棧對象有自動管理功能。那么棧對象會在什么會自動釋放了?第一,在其 生命期結束的時候;第二,在其所在的函數發生異常的時候。你也許說,這些都很正常啊,沒什么大不了的。是的,沒什么大不了的。但是只要我們再深入一點點, 也許就有意外的收獲了。

      棧對象,自動釋放時,會調用它自己的析構函數。如果我們在棧對象中封裝資源,而且在棧對象的析構函數中執行釋放資源的動作,那么就會使資源泄漏的概率 大大降低,因為棧對象可以自動的釋放資源,即使在所在函數發生異常的時候。實際的過程是這樣的:函數拋出異常時,會發生所謂的 stack_unwinding(堆棧回滾),即堆棧會展開,由于是棧對象,自然存在于棧中,所以在堆棧回滾的過程中,棧對象的析構函數會被執行,從而釋 放其所封裝的資源。除非,除非在析構函數執行的過程中再次拋出異常――而這種可能性是很小的,所以用棧對象封裝資源是比較安全的。基于此認識,我們就可以 創建一個自己的句柄或代理來封裝資源了。智能指針(auto_ptr)中就使用了這種技術。在有這種需要的時候,我們就希望我們的資源封裝類只能在棧中創 建,也就是要限制在堆中創建該資源封裝類的實例。

    3.1.4 禁止產生堆對象

      上面已經提到,你決定禁止產生某種類型的堆對象,這時你可以自己創建一個資源封裝類,該類對象只能在棧中產生,這樣就能在異常的情況下自動釋放封裝的資源。

      那么怎樣禁止產生堆對象了?我們已經知道,產生堆對象的唯一方法是使用new操作,如果我們禁止使用new不就行了么。再進一步,new操作執行時會 調用operator new,而operator new是可以重載的。方法有了,就是使new operator 為private,為了對稱,最好將operator delete也重載為private。現在,你也許又有疑問了,難道創建棧對象不需要調用new嗎?是的,不需要,因為創建棧對象不需要搜索內存,而是直 接調整堆棧指針,將對象壓棧,而operator new的主要任務是搜索合適的堆內存,為堆對象分配空間,這在上面已經提到過了。好,讓我們看看下面的示例代碼:

  • #include <stdlib.h> //需要用到C式內存分配函數
  • class Resource ; //代表需要被封裝的資源類
  • class NoHashObject
  • {
  •  private:
  •   Resource* ptr ;//指向被封裝的資源
  •   ... ... //其它數據成員
  •   void* operator new(size_t size) //非嚴格實現,僅作示意之用
  •   {
  •    return malloc(size) ;
  •   }
  •   void operator delete(void* pp) //非嚴格實現,僅作示意之用
  •   {
  •    free(pp) ;
  •   }
  •  public:
  •   NoHashObject()
  •   {
  •    //此處可以獲得需要封裝的資源,并讓ptr指針指向該資源
  •    ptr = new Resource() ;
  •   }
  •   ~NoHashObject()
  •   {
  •    delete ptr ; //釋放封裝的資源
  •   }
  • };
  • 復制代碼

    NoHashObject現在就是一個禁止堆對象的類了,如果你寫下如下代碼:

  • NoHashObject* fp = new NoHashObject() ; //編譯期錯誤!
  • delete fp ;
  • 復制代碼

    上 面代碼會產生編譯期錯誤。好了,現在你已經知道了如何設計一個禁止堆對象的類了,你也許和我一樣有這樣的疑問,難道在類NoHashObject 的定義不能改變的情況下,就一定不能產生該類型的堆對象了嗎?不,還是有辦法的,我稱之為“暴力破解法”。C++是如此地強大,強大到你可以用它做你想做 的任何事情。這里主要用到的是技巧是指針類型的強制轉換。

  • void main(void)
  • {
  •  char* temp = new char[sizeof(NoHashObject)] ;
  •  //強制類型轉換,現在ptr是一個指向NoHashObject對象的指針
  •  NoHashObject* obj_ptr = (NoHashObject*)temp ;
  •  temp = NULL ; //防止通過temp指針修改NoHashObject對象
  •  //再一次強制類型轉換,讓rp指針指向堆中NoHashObject對象的ptr成員
  •  Resource* rp = (Resource*)obj_ptr ;
  •  //初始化obj_ptr指向的NoHashObject對象的ptr成員
  •  rp = new Resource() ;
  •  //現在可以通過使用obj_ptr指針使用堆中的NoHashObject對象成員了
  •  ... ...
  •  delete rp ;//釋放資源
  •  temp = (char*)obj_ptr ;
  •  obj_ptr = NULL ;//防止懸掛指針產生
  •  delete [] temp ;//釋放NoHashObject對象所占的堆空間。
  • }
  • 復制代碼

    上面的實現是麻煩的,而且這種實現方式幾乎不會在實踐中使用,但是我還是寫出來路,因為理解它,對于我們理解C++內存對象是有好處的。對于上面的這么多強制類型轉換,其最根本的是什么了?我們可以這樣理解:

      某塊內存中的數據是不變的,而類型就是我們戴上的眼鏡,當我們戴上一種眼鏡后,我們就會用對應的類型來解釋內存中的數據,這樣不同的解釋就得到了不同的信息。

      所謂強制類型轉換實際上就是換上另一副眼鏡后再來看同樣的那塊內存數據。

      另外要提醒的是,不同的編譯器對對象的成員數據的布局安排可能是不一樣的,比如,大多數編譯器將NoHashObject的ptr指針成員安排在對象空間的頭4個字節,這樣才會保證下面這條語句的轉換動作像我們預期的那樣執行:

  • Resource* rp = (Resource*)obj_ptr;
  • 復制代碼

    但是,并不一定所有的編譯器都是如此。

      既然我們可以禁止產生某種類型的堆對象,那么可以設計一個類,使之不能產生棧對象嗎?當然可以。

    3.1.5 禁止產生棧對象

      前面已經提到了,創建棧對象時會移動棧頂指針以“挪出”適當大小的空間,然后在這個空間上直接調用對應的構造函數以形成一個棧對象,而當函數返回時, 會調用其析構函數釋放這個對象,然后再調整棧頂指針收回那塊棧內存。在這個過程中是不需要operator new/delete操作的,所以將operator new/delete設置為private不能達到目的。當然從上面的敘述中,你也許已經想到了:將構造函數或析構函數設為私有的,這樣系統就不能調用構 造/析構函數了,當然就不能在棧中生成對象了。

      這樣的確可以,而且我也打算采用這種方案。但是在此之前,有一點需要考慮清楚,那就是,如果我們將構造函數設置為私有,那么我們也就不能用 new來直接產生堆對象了,因為new在為對象分配空間后也會調用它的構造函數啊。所以,我打算只將析構函數設置為private。再進一步,將析構函數 設為private除了會限制棧對象生成外,還有其它影響嗎?是的,這還會限制繼承。

      如果一個類不打算作為基類,通常采用的方案就是將其析構函數聲明為private。

      為了限制棧對象,卻不限制繼承,我們可以將析構函數聲明為protected,這樣就兩全其美了。如下代碼所示:

  • class NoStackObject
  • {
  •  protected:
  •   ~NoStackObject() { }
  •  public:
  •   void destroy()
  •   {
  •    delete this ;//調用保護析構函數
  •   }
  • };
  • 復制代碼

    接著,可以像這樣使用NoStackObject類:

  • NoStackObject* hash_ptr = new NoStackObject() ;
  • ... ... //對hash_ptr指向的對象進行操作
  • hash_ptr->destroy() ;
  • 復制代碼

    呵 呵,是不是覺得有點怪怪的,我們用new創建一個對象,卻不是用delete去刪除它,而是要用destroy方法。很顯然,用戶是不習慣這種怪異的使用 方式的。所以,我決定將構造函數也設為private或protected。這又回到了上面曾試圖避免的問題,即不用new,那么該用什么方式來生成一個 對象了?我們可以用間接的辦法完成,即讓這個類提供一個static成員函數專門用于產生該類型的堆對象。(設計模式中的singleton 模式就可以用這種方式實現。)讓我們來看看:

  • class NoStackObject
  • {
  •  protected:
  •   NoStackObject() { }
  •   ~NoStackObject() { }
  •  public:
  •   static NoStackObject* creatInstance()
  •   {
  •    return new NoStackObject() ;//調用保護的構造函數
  •   }
  •   void destroy()
  •   {
  •    delete this ;//調用保護的析構函數
  •   }
  • };
  • 復制代碼

    現在可以這樣使用NoStackObject類了:

  • NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
  • ... ... //對hash_ptr指向的對象進行操作
  • hash_ptr->destroy() ;
  • hash_ptr = NULL ; //防止使用懸掛指針
  • 復制代碼

    現在感覺是不是好多了,生成對象和釋放對象的操作一致了。

    3.2 淺議C++ 中的垃圾回收方法

      許多 C 或者 C++ 程序員對垃圾回收嗤之以鼻,認為垃圾回收肯定比自己來管理動態內存要低效,而且在回收的時候一定會讓程序停頓在那里,而如果自己控制內存管理的話,分配和 釋放時間都是穩定的,不會導致程序停頓。最后,很多 C/C++ 程序員堅信在C/C++ 中無法實現垃圾回收機制。這些錯誤的觀點都是由于不了解垃圾回收的算法而臆想出來的。

      其實垃圾回收機制并不慢,甚至比動態內存分配更高效。因為我們可以只分配不釋放,那么分配內存的時候只需要從堆上一直的獲得新的內存,移動堆頂的指針 就夠了;而釋放的過程被省略了,自然也加快了速度。現代的垃圾回收算法已經發展了很多,增量收集算法已經可以讓垃圾回收過程分段進行,避免打斷程序的運行 了。而傳統的動態內存管理的算法同樣有在適當的時間收集內存碎片的工作要做,并不比垃圾回收更有優勢。

      而垃圾回收的算法的基礎通常基于掃描并標記當前可能被使用的所有內存塊,從已經被分配的所有內存中把未標記的內存回收來做的。C/C++ 中無法實現垃圾回收的觀點通常基于無法正確掃描出所有可能還會被使用的內存塊,但是,看似不可能的事情實際上實現起來卻并不復雜。首先,通過掃描內存的數 據,指向堆上動態分配出來內存的指針是很容易被識別出來的,如果有識別錯誤,也只能是把一些不是指針的數據當成指針,而不會把指針當成非指針數據。這樣, 回收垃圾的過程只會漏回收掉而不會錯誤的把不應該回收的內存清理。其次,如果回溯所有內存塊被引用的根,只可能存在于全局變量和當前的棧內,而全局變量 (包括函數內的靜態變量)都是集中存在于 bss 段或 data段中。

      垃圾回收的時候,只需要掃描 bss 段, data 段以及當前被使用著的棧空間,找到可能是動態內存指針的量,把引用到的內存遞歸掃描就可以得到當前正在使用的所有動態內存了。

      如果肯為你的工程實現一個不錯的垃圾回收器,提高內存管理的速度,甚至減少總的內存消耗都是可能的。如果有興趣的話,可以搜索一下網上已有的關于垃圾回收的論文和實現了的庫,開拓視野對一個程序員尤為重要。

    發表于 @ 2010年07月02日 09:27:00?|?評論(?0?)?|?舉報|?收藏

    總結

    以上是生活随笔為你收集整理的C语言运行时库详解的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    91理论片午午伦夜理片久久 | 成人在线播放免费观看 | 免费看黄在线观看 | 久久久一本精品99久久精品 | 96av麻豆蜜桃一区二区 | 香蕉视频亚洲 | 色婷婷88av视频一二三区 | 最新国产中文字幕 | 夜夜夜夜爽 | 911久久香蕉国产线看观看 | 婷婷六月天在线 | av福利在线免费观看 | 91麻豆精品国产自产在线游戏 | 日日干 天天干 | 91亚洲免费 | 日韩中文字幕一区 | 国产黄色一级片在线 | 国产精品av久久久久久无 | 久久久久久网址 | 国产精品毛片久久蜜 | 日日夜操 | 久久国语 | 久久中文字幕视频 | 天天干夜夜爽 | 国产一性一爱一乱一交 | 91亚洲精品国偷拍自产在线观看 | 国产一区二区高清视频 | 亚洲三级黄 | 天天躁日日躁狠狠 | 国产高清视频网 | 四虎5151久久欧美毛片 | 麻豆免费在线视频 | 久久精品导航 | 久久99亚洲精品久久久久 | 最近中文字幕免费大全 | 国产1区2区3区精品美女 | 亚洲国产字幕 | 亚洲精品在线视频观看 | 久久a久久 | 欧美日韩精品影院 | 日韩精品无 | 成人av在线影院 | 成人午夜电影免费在线观看 | 激情综合网天天干 | 亚洲人成影院在线 | 国产成人福利在线 | 午夜av一区| 成人av免费网站 | 91九色porny蝌蚪主页 | 日韩中文字幕免费在线观看 | 久草在线费播放视频 | 精品美女国产在线 | 成年人免费看的视频 | 99久久er热在这里只有精品66 | 美女视频网站久久 | 国产第一二区 | 久久精品99国产精品日本 | 国产精品porn | 日日夜夜天天操 | 成年人免费电影在线观看 | 色国产在线 | 五月天激情婷婷 | 狠狠狠操| 久久高视频 | 天天操天天爱天天爽 | 人人爽人人澡人人添人人人人 | 夜色成人网 | 四虎影视久久久 | 日日操天天操夜夜操 | 日韩三级视频在线看 | 久久久免费视频播放 | 国产一区免费 | 天天射天天操天天色 | 中文字幕在线观看免费高清电影 | www.五月婷| 免费a v观看 | 日韩免费一二三区 | 丁香激情综合 | 中文字幕免费高清在线观看 | 中文字幕一区二区三区乱码不卡 | 狠狠干夜夜操 | 国产精品99久久久久久久久久久久 | 久久久久久久久爱 | 中文字幕在线观看播放 | 久草精品视频在线观看 | 国产成人久久精品77777 | 中文字幕中文字幕在线中文字幕三区 | 依人成人综合网 | 国产你懂的在线 | 久久精品视频播放 | 久久久影院一区二区三区 | 久久亚洲影视 | 97色综合 | 激情久久一区二区三区 | 精品国产一区二区三区四区vr | 看av在线 | 久久99久久99精品免费看小说 | 国产99久久久久 | 一二区精品 | 精品国产一区二区三区久久影院 | 日韩在线视频网站 | 色中射 | 欧美日韩视频在线一区 | 国产成人亚洲精品自产在线 | 国产第一页福利影院 | 亚洲最大av网站 | 欧美激情精品一区 | 99久久精品无免国产免费 | 久久怡红院 | 夜夜夜影院 | 新版资源中文在线观看 | 亚洲国产精彩中文乱码av | 婷婷色在线观看 | 久久免费av电影 | 欧美日本在线视频 | 日本69hd| 国产成本人视频在线观看 | 欧美性超爽 | 日韩精品久久一区二区 | 国产又粗又硬又爽的视频 | 一区二区视频在线免费观看 | 日韩成人免费在线观看 | 久久综合色天天久久综合图片 | 免费福利在线 | 亚洲永久国产精品 | 看黄色.com| 精品一区欧美 | 五月网婷婷 | 亚洲中字幕 | 日韩亚洲在线视频 | 日韩啪啪小视频 | 欧美 日韩 性| 亚洲精选视频在线 | 在线观看片 | 亚洲第一中文网 | 欧美日本不卡高清 | 在线看片日韩 | 欧美少妇18p | 91综合视频在线观看 | 五月开心婷婷网 | 中文字幕 国产视频 | 在线国产视频观看 | 国产黄影院色大全免费 | www.亚洲视频| 91精品一区国产高清在线gif | av网站大全免费 | 亚洲精品久久久久中文字幕二区 | 亚洲天堂va| 中文字幕在线免费播放 | 91视频高清| 免费在线激情电影 | 天天操天天爱天天干 | 国产精品黄色 | 五月激情婷婷丁香 | 久久综合中文字幕 | 亚洲日本中文字幕在线观看 | 国产精品久久久久久久毛片 | 日韩一区二区免费视频 | 超碰在线色 | 亚洲男男gaygay无套同网址 | av中文字幕在线观看网站 | 亚洲黄色成人av | 国产精品av久久久久久无 | 日韩色中色 | 久久精品国产美女 | 欧美一级视频免费看 | 成人av影视 | 国产精品久久久久av免费 | 国产一区二区视频在线 | 99久久网站 | 在线日本看片免费人成视久网 | 国产精品午夜久久久久久99热 | 欧美做受69| 日韩电影中文,亚洲精品乱码 | 综合铜03 | 国产在线v| 在线看国产日韩 | 中文字幕999| 欧美日韩国产页 | 999久久久久久久久6666 | 激情网站免费观看 | 久久人人爽| 久久久久久久久久久久久久av | av网站大全免费 | 久久在线观看 | 中文字幕在线观看视频网站 | 国产很黄很色的视频 | 国产123区在线观看 国产精品麻豆91 | 91九色综合 | 一区二区视 | 999国内精品永久免费视频 | 国产精品18久久久久白浆 | 国产精品毛片久久久久久久久久99999999 | 99久久精品免费看 | 国产伦理一区二区 | 午夜精品久久久久久 | 天天操狠狠操夜夜操 | 天天射天天操天天 | 99久久精品免费看国产免费软件 | 97超碰在线资源 | 中文字幕 第二区 | 日韩中文字幕免费在线播放 | 不卡av电影在线观看 | 欧美最猛性xxxxx免费 | 日韩精品在线免费播放 | 人人爽人人爽人人片av免 | 久久综合久久综合这里只有精品 | 最新99热| 国产福利精品视频 | 亚洲伊人av | 久久久久国产一区二区三区 | 国产精品久久久久高潮 | 日韩欧美在线一区 | 久久免费成人精品视频 | 久久精品播放 | 天天色天天草天天射 | 首页国产精品 | 国产精品一级视频 | 成人免费观看在线视频 | 婷婷伊人综合 | 国产亚洲精品中文字幕 | 久久婷婷精品 | 亚洲开心激情 | 国产精品久久久久久久久久久久午 | 欧美性黑人 | 天天操天天射天天操 | 九九视频精品免费 | 国产乱对白刺激视频在线观看女王 | 国产亚洲精品成人av久久ww | 在线观看黄色免费视频 | 国色天香第二季 | 欧美a视频在线观看 | 亚洲美女精品 | 五月天综合网站 | 国产在线观看黄 | 玖玖玖国产精品 | 国产高清免费观看 | 亚洲精品自在在线观看 | 国产伦精品一区二区三区在线 | 国产女人18毛片水真多18精品 | 欧美日高清视频 | 久久久精品 | 中文视频在线播放 | 日韩av一区二区在线播放 | 99热在线国产精品 | 亚洲a资源 | 中文字幕网站 | 久草电影网| av中文字幕免费在线观看 | 天天射网| 久草资源免费 | 91chinese在线 | 久久久久影视 | 色综合久久88色综合天天人守婷 | 97在线观看免费观看 | 亚洲精品美女视频 | 久久有精品 | 99精品视频免费 | 国产一区二区高清 | av免费在线观看网站 | 一区在线观看 | 在线亚洲人成电影网站色www | 美女免费视频一区 | 狠狠色伊人亚洲综合成人 | 91av短视频| 超碰免费久久 | 伊人天堂网 | 日韩精品免费在线播放 | 一区二区三区免费在线观看视频 | 中文字幕在线观看视频免费 | 96国产精品| 久久久国内精品 | 精品久久久久久亚洲综合网站 | 国产二区电影 | 亚洲视频每日更新 | www.国产高清 | 精品亚洲男同gayvideo网站 | 国内精品久久久久久久影视简单 | 国产精品一区二区免费在线观看 | 在线观看日本韩国电影 | 国产午夜在线观看视频 | 亚州精品一二三区 | 国产二级视频 | 精品一二三四五区 | av免费成人| 成人黄色毛片 | 丁香婷婷激情网 | 国产午夜亚洲精品 | 久久9视频 | 久久欧美在线电影 | 日本午夜在线亚洲.国产 | 国产在线1区 | 91精品一区二区在线观看 | 精品91久久久久 | 免费色网站 | 黄色视屏免费在线观看 | 五月天综合 | 久久久精品一区二区三区 | 久久亚洲区| 久久久精品欧美一区二区免费 | 国内精品久久久久久久久久 | 欧美日韩国产一区二区三区在线观看 | 国产午夜av | 在线视频日韩一区 | 国产精品va在线观看入 | 国产自偷自拍 | 手机看片国产 | 免费在线观看av电影 | 激情五月***国产精品 | 免费精品国产 | 综合久久综合久久 | 精品欧美乱码久久久久久 | 久久亚洲欧美 | 国产黄色在线观看 | 久久丁香 | 亚洲欧美日本国产 | 狠狠干婷婷色 | 香蕉影视在线观看 | 亚洲免费视频观看 | 亚洲第一成网站 | 国产精品xxxx18a99| 国产日韩在线播放 | 日韩成人邪恶影片 | 九九综合在线 | 深爱激情五月婷婷 | 亚洲欧美视频在线 | 日韩欧美电影在线 | 激情五月婷婷激情 | 欧美精品国产综合久久 | 热99久久精品 | 免费合欢视频成人app | 国产a网站 | 在线不卡中文字幕播放 | 久久精品日本啪啪涩涩 | 色婷婷成人网 | www.国产视频 | 福利视频网站 | 91av视频免费观看 | 亚洲国产小视频在线观看 | 久久精品视频免费观看 | av一区二区三区在线观看 | 国产精品色婷婷 | 亚洲精品在线观看av | 98福利在线 | 日韩精品免费在线观看 | 成人黄色在线观看视频 | 欧美精品在线观看一区 | 色综合天天天天做夜夜夜夜做 | 99久久久国产精品美女 | 香蕉视频在线网站 | 日韩大片免费观看 | 国产一级电影在线 | 日本中文字幕网站 | 美国av大片 | 超碰人人干人人 | 欧美大片第1页 | 精品国产一区二区三区久久久 | 97色涩 | 精品色999 | 色综合五月天 | 国产97av| 欧美va天堂在线电影 | 91麻豆网站 | 日韩天天操 | a级一a一级在线观看 | 色婷婷狠狠干 | 日韩在线电影一区二区 | 99久久国产免费,99久久国产免费大片 | 欧美久草视频 | 婷婷丁香激情 | 国产一级性生活视频 | 国产在线精品播放 | 国产精品对白一区二区三区 | 日韩免费在线观看视频 | 一区二区三区视频 | 9ⅰ精品久久久久久久久中文字幕 | 欧美国产一区二区 | 亚洲激情网站免费观看 | 久久久久久久久久久久亚洲 | 91av99| 久久久久久久久久久久久久免费看 | 日韩久久视频 | 一区二区三区日韩精品 | 欧美一级大片在线观看 | 国产91对白在线 | 国产第一页福利影院 | 久久久国产日韩 | 久久久久五月天 | 久久激情片 | 麻豆国产在线视频 | 国产中年夫妇高潮精品视频 | 99热这里只有精品在线观看 | 日日夜夜狠狠干 | 日韩二区在线观看 | 亚洲视频综合 | 久久久久观看 | 中文字幕中文字幕在线中文字幕三区 | 玖玖玖国产精品 | 青青草国产成人99久久 | 欧美激情综合五月 | 亚洲一区二区视频 | 久久久久婷 | 97国产电影 | 精品国产大片 | 99亚洲天堂| 亚洲精品91天天久久人人 | 日韩一级片网址 | 国产一性一爱一乱一交 | 久久激情婷婷 | 久久久福利影院 | 欧美色图p | 国产精品永久免费视频 | 91九色最新地址 | 欧美网址在线观看 | 日日射av | 国产在线国偷精品产拍免费yy | 久久久麻豆 | 中文字幕美女免费在线 | 色视频在线观看免费 | 国产亚洲精品美女久久 | 91av在线视频免费观看 | 日韩精品黄 | 中文字幕在线人 | 亚洲最快最全在线视频 | 欧美日韩中文字幕综合视频 | 在线观看黄色小视频 | 丁香av | 欧美日韩在线视频一区二区 | 精品欧美一区二区三区久久久 | 天天天色综合a | 亚洲欧美国产精品18p | 日韩在线视频免费看 | 9999免费视频 | 天天摸天天舔天天操 | 最新中文字幕视频 | 亚洲成a人片77777潘金莲 | 69中文字幕 | 婷婷丁香色 | 成人免费网站在线观看 | 日产中文字幕 | 一区二区三区四区精品视频 | 成人黄色小视频 | 综合网伊人 | 国产精品视频大全 | 国产成人三级在线播放 | 婷婷色吧| 午夜影视剧场 | 精品美女视频 | 99国产一区二区三精品乱码 | 亚洲一区精品人人爽人人躁 | 91在线91拍拍在线91 | 92精品国产成人观看免费 | 91| 久久久电影 | 中文字幕在线播放日韩 | 国产丝袜一区二区三区 | 国产一区二区免费在线观看 | 江苏妇搡bbbb搡bbbb | 人人讲| 日韩一区二区在线免费观看 | 色中文字幕在线观看 | 超碰在线最新 | 色狠狠久久av五月综合 | 一区二区三区四区精品 | 国产美女免费看 | av电影免费在线看 | 91网址在线| 亚洲精品乱码久久久久久写真 | 黄色特级片| 久久免费精品视频 | 日韩欧美精品一区 | 国产精品电影一区 | 99久久9 | 亚洲精品美女久久17c | 天天天天天天操 | 免费观看日韩av | av网站播放 | 欧美激情精品久久久久久 | av福利在线| 婷婷六月丁 | 182午夜在线观看 | 久久成人国产精品入口 | 18+视频网站链接 | 天天色天天操综合网 | 亚洲三级网站 | 欧美另类交人妖 | 免费在线国产精品 | 波多野结衣网址 | 美女精品久久久 | 啪啪肉肉污av国网站 | 91精品第一页 | 久久爱导航 | 国产精品毛片一区视频播不卡 | 在线看的av网站 | 亚洲天堂网站视频 | 国产伦精品一区二区三区高清 | 亚洲aⅴ免费在线观看 | 波多野结衣最新 | 国产成人精品一区二区三区在线观看 | 国产精品一区二区在线观看 | 在线电影日韩 | 国产情侣一区 | 干天天| av播放在线| 超碰在线个人 | 99精品在线免费观看 | 一区三区视频在线观看 | 欧美日韩高清一区二区 | 国产啊v在线观看 | www.成人久久 | 97热在线观看 | 伊人久久影视 | 在线导航av| 国产麻豆果冻传媒在线观看 | 最新av网址在线 | 麻豆一区在线观看 | av视屏在线| 中文一区二区三区在线观看 | 欧美a级片免费看 | 奇米影视777四色米奇影院 | 丁香 久久 综合 | 久久精品国产成人精品 | 色欧美88888久久久久久影院 | 久久社区视频 | 免费性网站 | 99热这里只有精品8 久久综合毛片 | 丝袜美腿亚洲综合 | 国产中文字幕久久 | 国产在线观看一区 | 黄色三级网站在线观看 | 91在线porny国产在线看 | 成人av在线电影 | 成人9ⅰ免费影视网站 | 丁香六月激情婷婷 | 亚洲专区 国产精品 | 97在线观看免费视频 | 17婷婷久久www | 9热精品| 亚洲综合精品在线 | 新版资源中文在线观看 | 国产a高清| 国产视频91在线 | 久草com| 久草五月 | 亚洲激情p | 99色人 | 中文字幕高清有码 | 欧美一二三专区 | 国产专区免费 | 日本免费一二三区 | 超碰97网站 | 亚欧洲精品视频在线观看 | 久久精品国产亚洲精品2020 | 精品超碰 | 久久呀 | 国产精品一区欧美 | 久久久久久久久影院 | 亚洲成a人片77777潘金莲 | 国产高清视频在线免费观看 | 成人一区二区三区在线观看 | 国产精品久久久一区二区三区网站 | 91人网站 | 狠狠干夜夜操天天爽 | 中文字幕在线网 | 超碰av免费| 精品久操| 日日干日日 | 激情av五月婷婷 | 国产最新福利 | 激情黄色av| 午夜精品久久久久久久99 | 一区二区电影网 | 91chinese在线| 欧洲精品久久久久毛片完整版 | 在线观看国产日韩 | 婷婷四房综合激情五月 | 日韩影视在线 | 91影视成人| 99国产成+人+综合+亚洲 欧美 | 97超碰在线久草超碰在线观看 | 欧美激情综合五月 | www.黄色片.com | 日本韩国精品一区二区在线观看 | 国产精品手机在线观看 | 日韩网站免费观看 | 中文字幕一区二区三区久久 | 成人黄色大片在线观看 | 亚洲成人999 | 欧美久久久久久久久久久 | 日韩高清av在线 | 国产精品四虎 | 99免费精品视频 | 日韩特级毛片 | 91欧美日韩国产 | 国产精品久久久久久久免费大片 | 国产精品久久久久久一二三四五 | 国产国语在线 | 国产精品久久久久国产精品日日 | 在线a视频免费观看 | 国产精品电影在线 | 日韩国产欧美在线播放 | 天天操天天爽天天干 | 97在线观视频免费观看 | 色综合中文字幕 | 91久久久久久久一区二区 | 久久久精品国产一区二区电影四季 | 欧美日韩国产在线 | 激情五月播播久久久精品 | 在线观看黄色免费视频 | 精品久久久久久久久久久久久久久久久久 | 色视频成人在线观看免 | 亚洲精品九九 | 超碰97久久 | 婷婷激情五月综合 | 97精品国产97久久久久久粉红 | 国产区av在线| 久久精品这里热有精品 | 国产在线视频一区二区三区 | 91精品国产91久久久久福利 | 国产视频久久 | 国产精品欧美一区二区三区不卡 | 成年人三级网站 | 久草国产在线观看 | 日本中文字幕视频 | 最近中文字幕视频完整版 | 久久亚洲综合国产精品99麻豆的功能介绍 | 91在线免费视频 | 国产精品免费一区二区三区在线观看 | 最近中文字幕 | 色吊丝av中文字幕 | 91精品视频导航 | 成人在线视频在线观看 | 国产人成免费视频 | 三级av免费观看 | 久久精品爱爱视频 | 91视频在线免费 | 一级片视频在线 | 日韩精品视频在线观看免费 | 欧美精品日韩 | 99久久99久久 | 精品国产美女 | 四虎在线观看精品视频 | 日韩三级免费 | 欧美久久久久久久久久久久久 | 国产精品 国产精品 | 在线观看蜜桃视频 | 久久这里只精品 | 日韩欧美视频在线观看免费 | 在线观看一级片 | 日韩午夜大片 | 精品在线播放 | 麻豆视频免费在线播放 | 99久久精品国产免费看不卡 | 色综合咪咪久久网 | 国产精品久久久影视 | av色综合 | 91亚色视频 | 亚洲理论片在线观看 | 国产亚洲成av人片在线观看桃 | 永久av免费在线观看 | 午夜私人影院久久久久 | 欧美日韩国产一区二 | 日韩久久精品一区二区 | 日日爽夜夜操 | 日本中文字幕观看 | 91av99| 欧美日韩啪啪 | www欧美日韩 | 国产一级三级 | 97高清免费视频 | 亚洲成人动漫在线观看 | 欧美午夜精品久久久久久孕妇 | 成年人免费在线观看网站 | 国产 视频 久久 | 久久久久亚洲精品男人的天堂 | 91 在线视频 | 在线黄色av电影 | 国产明星视频三级a三级点| 精品国产三级a∨在线欧美 免费一级片在线观看 | 中文字幕av在线不卡 | 伊人开心激情 | 国产小视频在线播放 | 国产成人精品一二三区 | 免费黄色a网站 | 99视频+国产日韩欧美 | 黄av在线| 久久久久国产精品免费网站 | 波多野结衣日韩 | 成人宗合网 | 免费国产亚洲视频 | 永久免费毛片 | 国产精品久久一卡二卡 | 色综合激情久久 | 日韩免费专区 | 国产91精品看黄网站在线观看动漫 | 日韩丝袜视频 | 国产欧美精品在线观看 | 亚洲黄色app | 最新91在线视频 | av片子在线观看 | 国产手机视频精品 | 国产精品美女久久久久久2018 | 久久久一本精品99久久精品66 | 91精品一区在线观看 | 国产精品久久久久久欧美 | 国产一级视频在线免费观看 | 国产精品美女999 | 天天激情 | 免费av小说 | 国产精品美女久久久久久网站 | 91av欧美 | 国际精品久久久 | www.午夜色.com| 久久精品波多野结衣 | 久草网站在线观看 | 久久国产精品久久精品国产演员表 | 69国产精品视频免费观看 | 99色婷婷| 97人人超碰在线 | 手机成人免费视频 | 国产精品成人在线观看 | 波多野结衣在线视频免费观看 | 国内精品久久久 | 夜夜骑天天操 | 国产 视频 高清 免费 | 99久久久久久久久 | 亚洲视频精品在线 | 亚洲精品综合久久 | 日本精品在线看 | 免费看国产a | 69av久久| www日| 久久中文精品视频 | 色婷婷综合久久久中文字幕 | 999视频在线播放 | 最新色站 | www国产亚洲精品久久网站 | 丰满少妇对白在线偷拍 | 国内精品久久久久久久久 | 欧美 亚洲 另类 激情 另类 | 亚洲欧洲国产日韩精品 | 一区二区三区四区五区在线 | 在线免费观看的av | 香蕉在线视频播放网站 | 亚洲色图av| 久青草电影 | 日韩欧美精品一区二区三区经典 | 精品国产欧美 | 欧美三人交| 久草久草在线 | 日韩电影一区二区在线观看 | 欧美午夜剧场 | avhd高清在线谜片 | 97国产电影| 国产精品一区二区久久久 | 91精品在线播放 | 欧美a影视 | 久久免费观看少妇a级毛片 久久久久成人免费 | 欧美日韩免费一区 | 视频91 | 日韩av黄| 色吧av色av| 婷婷射五月 | 色在线视频网 | 亚洲激情综合网 | 天堂网一区二区 | 欧美一级视频免费看 | 日韩欧美一区二区不卡 | 欧美91精品久久久久国产性生爱 | 国产黄色大片 | 日韩中文字幕免费视频 | 国产青青青 | a午夜在线| 99热这里是精品 | 少妇高潮流白浆在线观看 | 色偷偷网站视频 | av成人动漫 | 亚洲人成人天堂h久久 | 国产真实精品久久二三区 | 国产香蕉久久精品综合网 | 欧美一级特黄高清视频 | 狠狠狠狠狠色综合 | 国产日韩在线播放 | 国产伦精品一区二区三区无广告 | 九九爱免费视频 | 免费在线一区二区三区 | 一区二精品 | av日韩精品 | 99综合影院在线 | 亚洲成人av电影在线 | 久久免费精品一区二区三区 | 美女在线观看网站 | 欧美巨乳波霸 | 九九99| 国产精品第二页 | 日韩r级在线 | 日韩影视精品 | 国产黄色片久久久 | 久久99视频免费 | av电影中文字幕 | 国产精品一区在线观看你懂的 | 久香蕉 | 国产最新91 | 三级av在线播放 | 免费看wwwwwwwwwww的视频 久久久久久99精品 91中文字幕视频 | 日韩av成人免费看 | 天天天插| 亚州黄色一级 | 婷婷激情5月天 | 五月婷婷欧美 | 久久激情小视频 | 美女视频免费精品 | 日韩精品专区 | 久久精品久久久久 | 亚洲国产成人精品在线观看 | 日韩在线播放欧美字幕 | 婷婷激情久久 | 在线色亚洲 | 2022久久国产露脸精品国产 | 国产96精品 | 99爱这里只有精品 | 欧美看片| 在线视频欧美亚洲 | 黄色在线观看免费网站 | 亚洲日本在线视频观看 | 97色婷婷成人综合在线观看 | 久久久污 | 91麻豆高清视频 | 国产 精品 资源 | 视频在线亚洲 | 亚洲美女精品区人人人人 | 97国产精品久久 | 亚洲黄色免费网站 | 国产黄色在线看 | 国产一区二区三区免费在线观看 | 日本乱视频| ww亚洲ww亚在线观看 | 成人在线播放免费观看 | 亚洲免费永久精品国产 | 国产视频观看 | 天天伊人狠狠 | 怡红院久久 | 色91在线视频 | 99视频免费播放 | 久草.com| 欧美日韩二区三区 | 日本中文字幕网址 | 精精国产xxxx视频在线播放 | 国产黄色精品在线观看 | 黄色三级免费观看 | h网站免费在线观看 | 黄色av观看 | 色婷婷欧美 | 精品国产乱子伦一区二区 | 成人中文字幕在线观看 | 日韩视频欧美视频 | 久久在线免费观看 | 亚洲女裸体 | 91精品在线免费观看 | 麻豆91在线| 在线亚洲欧美视频 | 超碰成人av| 天天视频色| 日韩三级视频在线观看 | 女人18毛片90分钟 | 又黄又爽免费视频 | 999成人精品 | 欧美日韩视频网站 | www免费看 | 五月开心六月婷婷 | 黄色视屏av| 亚洲视频免费在线观看 | 欧美在线aa | av一区在线播放 | 91色九色 | 亚洲色图美腿丝袜 | 国产精品成人一区二区三区吃奶 | 成人久久 | 久久激情影院 | 黄影院| 丁香六月网 | 天天夜夜操 | 在线观看成人小视频 | 99久久er热在这里只有精品15 | 黄色三级在线观看 | 五月天婷亚洲天综合网精品偷 | 91在线免费播放视频 | 在线播放视频一区 | 99视频在线观看视频 | 国产在线第三页 | 91免费观看网站 | 久久优 | 欧美激情精品久久久 | 激情婷婷久久 | 在线观看av麻豆 | 国产精品一区欧美 | 国产专区精品 | 免费网站污| 国产免费亚洲 | 狠色狠色综合久久 | 草久草久 | 在线成人一区 | 2021国产视频 | 黄色成人av网址 | 国产高清久久久久 | 亚洲一级电影 | 狠狠插狠狠干 | 久久久久这里只有精品 | www.黄色在线 | 色黄久久久久久 | 日韩一级片网址 | 性色av免费看 | 国产91精品看黄网站在线观看动漫 | 日日草夜夜操 | 国产精品久久久久国产a级 激情综合中文娱乐网 | 日韩精品你懂的 | 中文字幕丰满人伦在线 | 99精品黄色片免费大全 | 天天色综合天天 | 激情五月在线观看 | 亚洲免费不卡 | 国产手机在线观看 | 日韩欧美一区二区三区视频 | 国产aa免费视频 | 国产特级毛片 | 中文字幕在线观看国产 | 日韩精品欧美专区 | 亚洲久草视频 | 亚洲精品 在线视频 | 又爽又黄在线观看 | 午夜国产一区二区 | 国产精品99久久久久久宅男 | 欧美精品xxx | 午夜神马福利 | 婷婷精品在线视频 | www.五月天婷婷.com | 欧美成人精品欧美一级乱黄 | 视频三区在线 | 亚洲男男gaygayxxxgv | 亚洲人xxx| 国产亚洲精品久久久久久无几年桃 | 中文字幕成人在线 | 日韩av免费观看网站 | 国产日韩精品在线观看 | 一区二区三区免费在线观看视频 | 亚洲精品在线免费播放 | 精品专区一区二区 | 国产精品va | 99久久国产免费看 | 日韩videos高潮hd | 91专区在线观看 | 激情综合亚洲精品 | 成人超碰97 | 欧美一级在线观看视频 | 中文字幕在线网 | 国产精品一区二区三区99 | 国内精品视频久久 | 色综合久久五月天 | 久久人人爽人人爽人人片av软件 | 亚洲经典中文字幕 | 色小说av | 91精品无人成人www | 日日干天天 | 在线观看av免费 | 国产亚洲精品女人久久久久久 | 99视频| 成人啪啪18免费游戏链接 | 日韩免费观看av | 成人中文字幕av | 麻豆传媒视频在线免费观看 | 国产一区精品在线观看 | 久久久久成人免费 | 国产一级淫片免费看 | 精品一区二区综合 | 97超碰精品| 成人精品久久 | 精品国产一区二区三区久久久蜜臀 | 黄色三级免费片 | 久久精品视频免费观看 | 免费视频久久久 | 四虎亚洲精品 | 久久久色 | 在线播放视频一区 | 一区二区亚洲精品 | 免费在线一区二区三区 | 91免费视频网站在线观看 | 国产一级一片免费播放放 | 天天草av | 天天综合五月天 | 99这里只有久久精品视频 | 亚洲精品午夜久久久久久久 | 亚洲a成人v| 国内外成人免费在线视频 | 日本久久91 | www.eeuss影院av撸 | 亚洲精品97 | 亚洲精品乱码久久久久久 | 国内精品久久久久久久影视麻豆 |