成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

實(shí)例講解代碼之內(nèi)存安全與效率

開(kāi)發(fā) 前端
C 是一種高級(jí)語(yǔ)言,同時(shí)具有“接近金屬close-to-the-metal”(LCTT 譯注:即“接近人類思維方式”的反義詞)的特性,這使得它有時(shí)看起來(lái)更像是一種可移植的匯編語(yǔ)言,而不像 Java 或 Python 這樣的兄弟語(yǔ)言。

了解有關(guān)內(nèi)存安全和效率的更多信息。

C 是一種高級(jí)語(yǔ)言,同時(shí)具有“接近金屬close-to-the-metal”(LCTT 譯注:即“接近人類思維方式”的反義詞)的特性,這使得它有時(shí)看起來(lái)更像是一種可移植的匯編語(yǔ)言,而不像 Java 或 Python 這樣的兄弟語(yǔ)言。內(nèi)存管理作為上述特性之一,涵蓋了正在執(zhí)行的程序?qū)?nèi)存的安全和高效使用。本文通過(guò) C 語(yǔ)言代碼示例,以及現(xiàn)代 C 語(yǔ)言編譯器生成的匯編語(yǔ)言代碼段,詳細(xì)介紹了內(nèi)存安全性和效率。

[[427063]]

盡管代碼示例是用 C 語(yǔ)言編寫的,但安全高效的內(nèi)存管理指南對(duì)于 C++ 是同樣適用的。這兩種語(yǔ)言在很多細(xì)節(jié)上有所不同(例如,C++ 具有 C 所缺乏的面向?qū)ο筇匦院头盒?,但在內(nèi)存管理方面面臨的挑戰(zhàn)是一樣的。

執(zhí)行中程序的內(nèi)存概述

對(duì)于正在執(zhí)行的程序(又名 進(jìn)程process),內(nèi)存被劃分為三個(gè)區(qū)域:棧stack、堆heap 和 靜態(tài)區(qū)static area。下文會(huì)給出每個(gè)區(qū)域的概述,以及完整的代碼示例。

作為通用 CPU 寄存器的替補(bǔ),棧 為代碼塊(例如函數(shù)或循環(huán)體)中的局部變量提供暫存器存儲(chǔ)。傳遞給函數(shù)的參數(shù)在此上下文中也視作局部變量。看一下下面這個(gè)簡(jiǎn)短的示例:

  1. void some_func(int a, int b) { 
  2.    int n; 
  3.    ... 

通過(guò) a 和 b 傳遞的參數(shù)以及局部變量 n 的存儲(chǔ)會(huì)在棧中,除非編譯器可以找到通用寄存器。編譯器傾向于優(yōu)先將通用寄存器用作暫存器,因?yàn)?CPU 對(duì)這些寄存器的訪問(wèn)速度很快(一個(gè)時(shí)鐘周期)。然而,這些寄存器在臺(tái)式機(jī)、筆記本電腦和手持機(jī)器的標(biāo)準(zhǔn)架構(gòu)上很少(大約 16 個(gè))。

在只有匯編語(yǔ)言程序員才能看到的實(shí)施層面,棧被組織為具有 push(插入)和 pop(刪除)操作的 LIFO(后進(jìn)先出)列表。 top 指針可以作為偏移的基地址;這樣,除了 top 之外的棧位置也變得可訪問(wèn)了。例如,表達(dá)式 top+16 指向堆棧的 top 指針上方 16 個(gè)字節(jié)的位置,表達(dá)式 top-16 指向 top 指針下方 16 個(gè)字節(jié)的位置。因此,可以通過(guò) top 指針訪問(wèn)實(shí)現(xiàn)了暫存器存儲(chǔ)的棧的位置。在標(biāo)準(zhǔn)的 ARM 或 Intel 架構(gòu)中,棧從高內(nèi)存地址增長(zhǎng)到低內(nèi)存地址;因此,減小某進(jìn)程的 top 就是增大其棧規(guī)模。

使用棧結(jié)構(gòu)就意味著輕松高效地使用內(nèi)存。編譯器(而非程序員)會(huì)編寫管理?xiàng)5拇a,管理過(guò)程通過(guò)分配和釋放所需的暫存器存儲(chǔ)來(lái)實(shí)現(xiàn);程序員聲明函數(shù)參數(shù)和局部變量,將實(shí)現(xiàn)過(guò)程交給編譯器。此外,完全相同的棧存儲(chǔ)可以在連續(xù)的函數(shù)調(diào)用和代碼塊(如循環(huán))中重復(fù)使用。精心設(shè)計(jì)的模塊化代碼會(huì)將棧存儲(chǔ)作為暫存器的首選內(nèi)存選項(xiàng),同時(shí)優(yōu)化編譯器要盡可能使用通用寄存器而不是棧。

堆 提供的存儲(chǔ)是通過(guò)程序員代碼顯式分配的,堆分配的語(yǔ)法因語(yǔ)言而異。在 C 中,成功調(diào)用庫(kù)函數(shù) malloc(或其變體 calloc 等)會(huì)分配指定數(shù)量的字節(jié)(在 C++ 和 Java 等語(yǔ)言中,new 運(yùn)算符具有相同的用途)。編程語(yǔ)言在如何釋放堆分配的存儲(chǔ)方面有著巨大的差異:

在 Java、Go、Lisp 和 Python 等語(yǔ)言中,程序員不會(huì)顯式釋放動(dòng)態(tài)分配的堆存儲(chǔ)。

例如,下面這個(gè) Java 語(yǔ)句為一個(gè)字符串分配了堆存儲(chǔ),并將這個(gè)堆存儲(chǔ)的地址存儲(chǔ)在變量 greeting 中:

  1. String greeting = new String("Hello, world!"); 

Java 有一個(gè)垃圾回收器,它是一個(gè)運(yùn)行時(shí)實(shí)用程序,如果進(jìn)程無(wú)法再訪問(wèn)自己分配的堆存儲(chǔ),回收器可以使其自動(dòng)釋放。因此,Java 堆釋放是通過(guò)垃圾收集器自動(dòng)進(jìn)行的。在上面的示例中,垃圾收集器將在變量 greeting 超出作用域后,釋放字符串的堆存儲(chǔ)。

Rust 編譯器會(huì)編寫堆釋放代碼。這是 Rust 在不依賴?yán)厥掌鞯那闆r下,使堆釋放實(shí)現(xiàn)自動(dòng)化的開(kāi)創(chuàng)性努力,但這也會(huì)帶來(lái)運(yùn)行時(shí)復(fù)雜性和開(kāi)銷。向 Rust 的努力致敬!

在 C(和 C++)中,堆釋放是程序員的任務(wù)。程序員調(diào)用 malloc 分配堆存儲(chǔ),然后負(fù)責(zé)相應(yīng)地調(diào)用庫(kù)函數(shù) free 來(lái)釋放該存儲(chǔ)空間(在 C++ 中,new 運(yùn)算符分配堆存儲(chǔ),而 delete 和 delete[] 運(yùn)算符釋放此類存儲(chǔ))。下面是一個(gè) C 語(yǔ)言代碼示例:

  1. char* greeting = malloc(14);       /* 14 heap bytes */ 
  2. strcpy(greeting, "Hello, world!"); /* copy greeting into bytes */ 
  3. puts(greeting);                    /* print greeting */ 
  4. free(greeting);                    /* free malloced bytes */ 

C 語(yǔ)言避免了垃圾回收器的成本和復(fù)雜性,但也不過(guò)是讓程序員承擔(dān)了堆釋放的任務(wù)。

內(nèi)存的 靜態(tài)區(qū) 為可執(zhí)行代碼(例如 C 語(yǔ)言函數(shù))、字符串文字(例如“Hello, world!”)和全局變量提供存儲(chǔ)空間:

  1. int n;                       /* global variable */ 
  2. int main() {                 /* function */ 
  3.    char* msg = "No comment"; /* string literal */ 
  4.    ... 

該區(qū)域是靜態(tài)的,因?yàn)樗拇笮倪M(jìn)程執(zhí)行開(kāi)始到結(jié)束都固定不變。由于靜態(tài)區(qū)相當(dāng)于進(jìn)程固定大小的內(nèi)存占用,因此經(jīng)驗(yàn)法則是通過(guò)避免使用全局?jǐn)?shù)組等方法來(lái)使該區(qū)域盡可能小。

下文會(huì)結(jié)合代碼示例對(duì)本節(jié)概述展開(kāi)進(jìn)一步講解。

棧存儲(chǔ)

想象一個(gè)有各種連續(xù)執(zhí)行的任務(wù)的程序,任務(wù)包括了處理每隔幾分鐘通過(guò)網(wǎng)絡(luò)下載并存儲(chǔ)在本地文件中的數(shù)字?jǐn)?shù)據(jù)。下面的 stack 程序簡(jiǎn)化了處理流程(僅是將奇數(shù)整數(shù)值轉(zhuǎn)換為偶數(shù)),而將重點(diǎn)放在棧存儲(chǔ)的好處上。

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #define Infile   "incoming.dat" 
  4. #define Outfile  "outgoing.dat" 
  5. #define IntCount 128000  /* 128,000 */ 
  6. void other_task1() { /*...*/ } 
  7. void other_task2() { /*...*/ } 
  8. void process_data(const char* infile, 
  9.           const char* outfile, 
  10.           const unsigned n) { 
  11.   int nums[n]; 
  12.   FILE* input = fopen(infile, "r"); 
  13.   if (NULL == infile) return
  14.   FILE* output = fopen(outfile, "w"); 
  15.   if (NULL == output) { 
  16.     fclose(input); 
  17.     return
  18.   } 
  19.   fread(nums, n, sizeof(int), input); /* read input data */ 
  20.   unsigned i; 
  21.   for (i = 0; i < n; i++) { 
  22.     if (1 == (nums[i] & 0x1))  /* odd parity? */ 
  23.       nums[i]--;               /* make even */ 
  24.   } 
  25.   fclose(input);               /* close input file */ 
  26.   fwrite(nums, n, sizeof(int), output); 
  27.   fclose(output); 
  28. int main() { 
  29.   process_data(Infile, Outfile, IntCount); 
  30.    
  31.   /** now perform other tasks **/ 
  32.   other_task1(); /* automatically released stack storage available */ 
  33.   other_task2(); /* ditto */ 
  34.    
  35.   return 0; 

底部的 main 函數(shù)首先調(diào)用 process_data 函數(shù),該函數(shù)會(huì)創(chuàng)建一個(gè)基于棧的數(shù)組,其大小由參數(shù) n 給定(當(dāng)前示例中為 128,000)。因此,該數(shù)組占用 128000 * sizeof(int) 個(gè)字節(jié),在標(biāo)準(zhǔn)設(shè)備上達(dá)到了 512,000 字節(jié)(int 在這些設(shè)備上是四個(gè)字節(jié))。然后數(shù)據(jù)會(huì)被讀入數(shù)組(使用庫(kù)函數(shù) fread),循環(huán)處理,并保存到本地文件 outgoing.dat(使用庫(kù)函數(shù) fwrite)。

當(dāng) process_data 函數(shù)返回到其調(diào)用者 main 函數(shù)時(shí),process_data 函數(shù)的大約 500MB 棧暫存器可供 stack 程序中的其他函數(shù)用作暫存器。在此示例中,main 函數(shù)接下來(lái)調(diào)用存根函數(shù) other_task1 和 other_task2。這三個(gè)函數(shù)在 main 中依次調(diào)用,這意味著所有三個(gè)函數(shù)都可以使用相同的堆棧存儲(chǔ)作為暫存器。因?yàn)榫帉憲9芾泶a的是編譯器而不是程序員,所以這種方法對(duì)程序員來(lái)說(shuō)既高效又容易。

在 C 語(yǔ)言中,在塊(例如函數(shù)或循環(huán)體)內(nèi)定義的任何變量默認(rèn)都有一個(gè) auto 存儲(chǔ)類,這意味著該變量是基于棧的。存儲(chǔ)類 register 現(xiàn)在已經(jīng)過(guò)時(shí)了,因?yàn)?C 編譯器會(huì)主動(dòng)嘗試盡可能使用 CPU 寄存器。只有在塊內(nèi)定義的變量可能是 register,如果沒(méi)有可用的 CPU 寄存器,編譯器會(huì)將其更改為 auto。基于棧的編程可能是不錯(cuò)的首選方式,但這種風(fēng)格確實(shí)有一些挑戰(zhàn)性。下面的 badStack 程序說(shuō)明了這點(diǎn)。

  1. #include <stdio.h>; 
  2. const int* get_array(const unsigned n) { 
  3.   int arr[n]; /* stack-based array */ 
  4.   unsigned i; 
  5.   for (i = 0; i < n; i++) arr[i] = 1 + 1; 
  6.   return arr;  /** ERROR **/ 
  7. int main() { 
  8.   const unsigned n = 16; 
  9.   const int* ptr = get_array(n); 
  10.    
  11.   unsigned i; 
  12.   for (i = 0; i < n; i++) printf("%i ", ptr[i]); 
  13.   puts("\n"); 
  14.   return 0; 

badStack 程序中的控制流程很簡(jiǎn)單。main 函數(shù)使用 16(LCTT 譯注:原文為 128,應(yīng)為作者筆誤)作為參數(shù)調(diào)用函數(shù) get_array,然后被調(diào)用函數(shù)會(huì)使用傳入?yún)?shù)來(lái)創(chuàng)建對(duì)應(yīng)大小的本地?cái)?shù)組。get_array 函數(shù)會(huì)初始化數(shù)組并返回給 main 中的數(shù)組標(biāo)識(shí)符 arr。 arr 是一個(gè)指針常量,保存數(shù)組的第一個(gè) int 元素的地址。

當(dāng)然,本地?cái)?shù)組 arr 可以在 get_array 函數(shù)中訪問(wèn),但是一旦 get_array 返回,就不能合法訪問(wèn)該數(shù)組。盡管如此,main 函數(shù)會(huì)嘗試使用函數(shù) get_array 返回的堆棧地址 arr 來(lái)打印基于棧的數(shù)組。現(xiàn)代編譯器會(huì)警告錯(cuò)誤。例如,下面是來(lái)自 GNU 編譯器的警告:

  1. badStack.c: In function 'get_array'
  2. badStack.c:9:10: warning: function returns address of local variable [-Wreturn-local-addr] 
  3. return arr;  /** ERROR **/ 

一般規(guī)則是,如果使用棧存儲(chǔ)實(shí)現(xiàn)局部變量,應(yīng)該僅在該變量所在的代碼塊內(nèi),訪問(wèn)這塊基于棧的存儲(chǔ)(在本例中,數(shù)組指針 arr 和循環(huán)計(jì)數(shù)器 i 均為這樣的局部變量)。因此,函數(shù)永遠(yuǎn)不應(yīng)該返回指向基于棧存儲(chǔ)的指針。

堆存儲(chǔ)

接下來(lái)使用若干代碼示例凸顯在 C 語(yǔ)言中使用堆存儲(chǔ)的優(yōu)點(diǎn)。在第一個(gè)示例中,使用了最優(yōu)方案分配、使用和釋放堆存儲(chǔ)。第二個(gè)示例(在下一節(jié)中)將堆存儲(chǔ)嵌套在了其他堆存儲(chǔ)中,這會(huì)使其釋放操作變得復(fù)雜。

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. int* get_heap_array(unsigned n) { 
  4.   int* heap_nums = malloc(sizeof(int) * n);  
  5.    
  6.   unsigned i; 
  7.   for (i = 0; i < n; i++) 
  8.     heap_nums[i] = i + 1;  /* initialize the array */ 
  9.    
  10.   /* stack storage for variables heap_nums and i released 
  11.      automatically when get_num_array returns */ 
  12.   return heap_nums; /* return (copy of) the pointer */ 
  13. int main() { 
  14.   unsigned n = 100, i; 
  15.   int* heap_nums = get_heap_array(n); /* save returned address */ 
  16.    
  17.   if (NULL == heap_nums) /* malloc failed */ 
  18.     fprintf(stderr, "%s\n""malloc(...) failed..."); 
  19.   else { 
  20.     for (i = 0; i < n; i++) printf("%i\n", heap_nums[i]); 
  21.     free(heap_nums); /* free the heap storage */ 
  22.   } 
  23.   return 0;  

上面的 heap 程序有兩個(gè)函數(shù): main 函數(shù)使用參數(shù)(示例中為 100)調(diào)用 get_heap_array 函數(shù),參數(shù)用來(lái)指定數(shù)組應(yīng)該有多少個(gè) int 元素。因?yàn)槎逊峙淇赡軙?huì)失敗,main 函數(shù)會(huì)檢查 get_heap_array 是否返回了 NULL;如果是,則表示失敗。如果分配成功,main 將打印數(shù)組中的 int 值,然后立即調(diào)用庫(kù)函數(shù) free 來(lái)對(duì)堆存儲(chǔ)解除分配。這就是最優(yōu)的方案。

get_heap_array 函數(shù)以下列語(yǔ)句開(kāi)頭,該語(yǔ)句值得仔細(xì)研究一下:

  1. int* heap_nums = malloc(sizeof(int) * n); /* heap allocation */ 

malloc 庫(kù)函數(shù)及其變體函數(shù)針對(duì)字節(jié)進(jìn)行操作;因此,malloc 的參數(shù)是 n 個(gè) int 類型元素所需的字節(jié)數(shù)(sizeof(int) 在標(biāo)準(zhǔn)現(xiàn)代設(shè)備上是四個(gè)字節(jié))。malloc 函數(shù)返回所分配字節(jié)段的首地址,如果失敗則返回 NULL .

如果成功調(diào)用 malloc,在現(xiàn)代臺(tái)式機(jī)上其返回的地址大小為 64 位。在手持設(shè)備和早些時(shí)候的臺(tái)式機(jī)上,該地址的大小可能是 32 位,或者甚至更小,具體取決于其年代。堆分配數(shù)組中的元素是 int 類型,這是一個(gè)四字節(jié)的有符號(hào)整數(shù)。這些堆分配的 int 的地址存儲(chǔ)在基于棧的局部變量 heap_nums 中。可以參考下圖:

  1.             heap-based 
  2. k-based        / 
  3. \        +----+----+   +----+ 
  4. -nums--->|int1|int2|...|intN| 
  5.          +----+----+   +----+ 

一旦 get_heap_array 函數(shù)返回,指針變量 heap_nums 的棧存儲(chǔ)將自動(dòng)回收——但動(dòng)態(tài) int 數(shù)組的堆存儲(chǔ)仍然存在,這就是 get_heap_array 函數(shù)返回這個(gè)地址(的副本)給 main 函數(shù)的原因:它現(xiàn)在負(fù)責(zé)在打印數(shù)組的整數(shù)后,通過(guò)調(diào)用庫(kù)函數(shù) free 顯式釋放堆存儲(chǔ):

  1. free(heap_nums); /* free the heap storage */ 

malloc 函數(shù)不會(huì)初始化堆分配的存儲(chǔ)空間,因此里面是隨機(jī)值。相比之下,其變體函數(shù) calloc 會(huì)將分配的存儲(chǔ)初始化為零。這兩個(gè)函數(shù)都返回 NULL 來(lái)表示分配失敗。

在 heap 示例中,main 函數(shù)在調(diào)用 free 后會(huì)立即返回,正在執(zhí)行的程序會(huì)終止,這會(huì)讓系統(tǒng)回收所有已分配的堆存儲(chǔ)。盡管如此,程序員應(yīng)該養(yǎng)成在不再需要時(shí)立即顯式釋放堆存儲(chǔ)的習(xí)慣。

嵌套堆分配

下一個(gè)代碼示例會(huì)更棘手一些。C 語(yǔ)言有很多返回指向堆存儲(chǔ)的指針的庫(kù)函數(shù)。下面是一個(gè)常見(jiàn)的使用情景:

1、C 程序調(diào)用一個(gè)庫(kù)函數(shù),該函數(shù)返回一個(gè)指向基于堆的存儲(chǔ)的指針,而指向的存儲(chǔ)通常是一個(gè)聚合體,如數(shù)組或結(jié)構(gòu)體:

  1. SomeStructure* ptr = lib_function(); /* returns pointer to heap storage */ 

2、 然后程序使用所分配的存儲(chǔ)。

3、 對(duì)于清理而言,問(wèn)題是對(duì) free 的簡(jiǎn)單調(diào)用是否會(huì)清理庫(kù)函數(shù)分配的所有堆分配存儲(chǔ)。例如,SomeStructure 實(shí)例可能有指向堆分配存儲(chǔ)的字段。一個(gè)特別麻煩的情況是動(dòng)態(tài)分配的結(jié)構(gòu)體數(shù)組,每個(gè)結(jié)構(gòu)體有一個(gè)指向又一層動(dòng)態(tài)分配的存儲(chǔ)的字段。下面的代碼示例說(shuō)明了這個(gè)問(wèn)題,并重點(diǎn)關(guān)注了如何設(shè)計(jì)一個(gè)可以安全地為客戶端提供堆分配存儲(chǔ)的庫(kù)。

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. typedef struct { 
  4.   unsigned id; 
  5.   unsigned len; 
  6.   float*   heap_nums; 
  7. } HeapStruct; 
  8. unsigned structId = 1; 
  9. HeapStruct* get_heap_struct(unsigned n) { 
  10.   /* Try to allocate a HeapStruct. */ 
  11.   HeapStruct* heap_struct = malloc(sizeof(HeapStruct)); 
  12.   if (NULL == heap_struct) /* failure? */ 
  13.     return NULL;           /* if so, return NULL */ 
  14.   /* Try to allocate floating-point aggregate within HeapStruct. */ 
  15.   heap_struct->heap_nums = malloc(sizeof(float) * n); 
  16.   if (NULL == heap_struct->heap_nums) {  /* failure? */ 
  17.     free(heap_struct);                   /* if so, first free the HeapStruct */ 
  18.     return NULL;                         /* then return NULL */ 
  19.   } 
  20.   /* Success: set fields */ 
  21.   heap_struct->id = structId++; 
  22.   heap_struct->len = n; 
  23.   return heap_struct; /* return pointer to allocated HeapStruct */ 
  24. void free_all(HeapStruct* heap_struct) { 
  25.   if (NULL == heap_struct) /* NULL pointer? */ 
  26.     return;                /* if so, do nothing */ 
  27.    
  28.   free(heap_struct->heap_nums); /* first free encapsulated aggregate */ 
  29.   free(heap_struct);            /* then free containing structure */   
  30. int main() { 
  31.   const unsigned n = 100; 
  32.   HeapStruct* hs = get_heap_struct(n); /* get structure with N floats */ 
  33.   /* Do some (meaningless) work for demo. */ 
  34.   unsigned i; 
  35.   for (i = 0; i < n; i++) hs->heap_nums[i] = 3.14 + (float) i; 
  36.   for (i = 0; i < n; i += 10) printf("%12f\n", hs->heap_nums[i]); 
  37.   free_all(hs); /* free dynamically allocated storage */ 
  38.    
  39.   return 0; 

上面的 nestedHeap 程序示例以結(jié)構(gòu)體 HeapStruct 為中心,結(jié)構(gòu)體中又有名為 heap_nums 的指針字段:

  1. typedef struct { 
  2.   unsigned id; 
  3.   unsigned len; 
  4.   float*   heap_nums; /** pointer **/ 
  5. } HeapStruct; 

函數(shù) get_heap_struct 嘗試為 HeapStruct 實(shí)例分配堆存儲(chǔ),這需要為字段 heap_nums 指向的若干個(gè) float 變量分配堆存儲(chǔ)。如果成功調(diào)用 get_heap_struct 函數(shù),并將指向堆分配結(jié)構(gòu)體的指針以 hs 命名,其結(jié)果可以描述如下:

  1. hs-->HeapStruct instance 
  2.         id 
  3.         len 
  4.         heap_nums-->N contiguous float elements 

在 get_heap_struct 函數(shù)中,第一個(gè)堆分配過(guò)程很簡(jiǎn)單:

  1. HeapStruct* heap_struct = malloc(sizeof(HeapStruct)); 
  2. if (NULL == heap_struct) /* failure? */ 
  3.   return NULL;           /* if so, return NULL */ 

sizeof(HeapStruct) 包括了 heap_nums 字段的字節(jié)數(shù)(32 位機(jī)器上為 4,64 位機(jī)器上為 8),heap_nums 字段則是指向動(dòng)態(tài)分配數(shù)組中的 float 元素的指針。那么,問(wèn)題關(guān)鍵在于 malloc 為這個(gè)結(jié)構(gòu)體傳送了字節(jié)空間還是表示失敗的 NULL;如果是 NULL,get_heap_struct 函數(shù)就也返回 NULL 以通知調(diào)用者堆分配失敗。

第二步嘗試堆分配的過(guò)程更復(fù)雜,因?yàn)樵谶@一步,HeapStruct 的堆存儲(chǔ)已經(jīng)分配好了:

  1. heap_struct->heap_nums = malloc(sizeof(float) * n); 
  2. if (NULL == heap_struct->heap_nums) {  /* failure? */ 
  3.   free(heap_struct);                   /* if so, first free the HeapStruct */ 
  4.   return NULL;                         /* and then return NULL */ 

傳遞給 get_heap_struct 函數(shù)的參數(shù) n 指明動(dòng)態(tài)分配的 heap_nums 數(shù)組中應(yīng)該有多少個(gè) float 元素。如果可以分配所需的若干個(gè) float 元素,則該函數(shù)在返回 HeapStruct 的堆地址之前會(huì)設(shè)置結(jié)構(gòu)的 id 和 len 字段。 但是,如果嘗試分配失敗,則需要兩個(gè)步驟來(lái)實(shí)現(xiàn)最優(yōu)方案:

  1. 必須釋放 HeapStruct 的存儲(chǔ)以避免內(nèi)存泄漏。對(duì)于調(diào)用 get_heap_struct 的客戶端函數(shù)而言,沒(méi)有動(dòng)態(tài) heap_nums 數(shù)組的 HeapStruct 可能就是沒(méi)用的;因此,HeapStruct 實(shí)例的字節(jié)空間應(yīng)該顯式釋放,以便系統(tǒng)可以回收這些空間用于未來(lái)的堆分配。
  2. 返回 NULL 以標(biāo)識(shí)失敗。

如果成功調(diào)用 get_heap_struct 函數(shù),那么釋放堆存儲(chǔ)也很棘手,因?yàn)樗婕耙哉_順序進(jìn)行的兩次 free 操作。因此,該程序設(shè)計(jì)了一個(gè) free_all 函數(shù),而不是要求程序員再去手動(dòng)實(shí)現(xiàn)兩步釋放操作。回顧一下,free_all 函數(shù)是這樣的:

  1. void free_all(HeapStruct* heap_struct) { 
  2.   if (NULL == heap_struct) /* NULL pointer? */ 
  3.     return;                /* if so, do nothing */ 
  4.    
  5.   free(heap_struct->heap_nums); /* first free encapsulated aggregate */ 
  6.   free(heap_struct);            /* then free containing structure */   

檢查完參數(shù) heap_struct 不是 NULL 值后,函數(shù)首先釋放 heap_nums 數(shù)組,這步要求 heap_struct 指針此時(shí)仍然是有效的。先釋放 heap_struct 的做法是錯(cuò)誤的。一旦 heap_nums 被釋放,heap_struct 就可以釋放了。如果 heap_struct 被釋放,但 heap_nums 沒(méi)有被釋放,那么數(shù)組中的 float 元素就會(huì)泄漏:仍然分配了字節(jié)空間,但無(wú)法被訪問(wèn)到——因此一定要記得釋放 heap_nums。存儲(chǔ)泄漏將一直持續(xù),直到 nestedHeap 程序退出,系統(tǒng)回收泄漏的字節(jié)時(shí)為止。

關(guān)于 free 庫(kù)函數(shù)的注意事項(xiàng)就是要有順序。回想一下上面的調(diào)用示例:

  1. free(heap_struct->heap_nums); /* first free encapsulated aggregate */ 
  2. free(heap_struct);            /* then free containing structure */ 

這些調(diào)用釋放了分配的存儲(chǔ)空間——但它們并 不是 將它們的操作參數(shù)設(shè)置為 NULL(free 函數(shù)會(huì)獲取地址的副本作為參數(shù);因此,將副本更改為 NULL 并不會(huì)改變?cè)刂飞系膮?shù)值)。例如,在成功調(diào)用 free 之后,指針 heap_struct 仍然持有一些堆分配字節(jié)的堆地址,但是現(xiàn)在使用這個(gè)地址將會(huì)產(chǎn)生錯(cuò)誤,因?yàn)閷?duì) free 的調(diào)用使得系統(tǒng)有權(quán)回收然后重用這些分配過(guò)的字節(jié)。

使用 NULL 參數(shù)調(diào)用 free 沒(méi)有意義,但也沒(méi)有什么壞處。而在非 NULL 的地址上重復(fù)調(diào)用 free 會(huì)導(dǎo)致不確定結(jié)果的錯(cuò)誤:

  1. free(heap_struct);  /* 1st call: ok */ 
  2. free(heap_struct);  /* 2nd call: ERROR */ 

內(nèi)存泄漏和堆碎片化

“內(nèi)存泄漏”是指動(dòng)態(tài)分配的堆存儲(chǔ)變得不再可訪問(wèn)。看一下相關(guān)的代碼段:

  1. float* nums = malloc(sizeof(float) * 10); /* 10 floats */ 
  2. nums[0] = 3.14f;                          /* and so on */ 
  3. nums = malloc(sizeof(float) * 25);        /* 25 new floats */ 

假如第一個(gè) malloc 成功,第二個(gè) malloc 會(huì)再將 nums 指針重置為 NULL(分配失敗情況下)或是新分配的 25 個(gè) float 中第一個(gè)的地址。最初分配的 10 個(gè) float 元素的堆存儲(chǔ)仍然處于被分配狀態(tài),但此時(shí)已無(wú)法再對(duì)其訪問(wèn),因?yàn)?nums 指針要么指向別處,要么是 NULL。結(jié)果就是造成了 40 個(gè)字節(jié)(sizeof(float) * 10)的泄漏。

在第二次調(diào)用 malloc 之前,應(yīng)該釋放最初分配的存儲(chǔ)空間:

  1. float* nums = malloc(sizeof(float) * 10); /* 10 floats */ 
  2. nums[0] = 3.14f;                          /* and so on */ 
  3. free(nums);                               /** good **/ 
  4. nums = malloc(sizeof(float) * 25);        /* no leakage */ 

即使沒(méi)有泄漏,堆也會(huì)隨著時(shí)間的推移而碎片化,需要對(duì)系統(tǒng)進(jìn)行碎片整理。例如,假設(shè)兩個(gè)最大的堆塊當(dāng)前的大小分別為 200MB 和 100MB。然而,這兩個(gè)堆塊并不連續(xù),進(jìn)程 P 此時(shí)又需要分配 250MB 的連續(xù)堆存儲(chǔ)。在進(jìn)行分配之前,系統(tǒng)可能要對(duì)堆進(jìn)行 碎片整理 以給 P 提供 250MB 連續(xù)存儲(chǔ)空間。碎片整理很復(fù)雜,因此也很耗時(shí)。

內(nèi)存泄漏會(huì)創(chuàng)建處于已分配狀態(tài)但不可訪問(wèn)的堆塊,從而會(huì)加速碎片化。因此,釋放不再需要的堆存儲(chǔ)是程序員幫助減少碎片整理需求的一種方式。

診斷內(nèi)存泄漏的工具

有很多工具可用于分析內(nèi)存效率和安全性,其中我最喜歡的是 valgrind。為了說(shuō)明該工具如何處理內(nèi)存泄漏,這里給出 leaky 示例程序:

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. int* get_ints(unsigned n) { 
  4.   int* ptr = malloc(n * sizeof(int)); 
  5.   if (ptr != NULL) { 
  6.     unsigned i; 
  7.     for (i = 0; i < n; i++) ptr[i] = i + 1; 
  8.   } 
  9.   return ptr; 
  10. void print_ints(int* ptr, unsigned n) { 
  11.   unsigned i; 
  12.   for (i = 0; i < n; i++) printf("%3i\n", ptr[i]); 
  13. int main() { 
  14.   const unsigned n = 32; 
  15.   int* arr = get_ints(n); 
  16.   if (arr != NULL) print_ints(arr, n); 
  17.   /** heap storage not yet freed... **/ 
  18.   return 0; 

main 函數(shù)調(diào)用了 get_ints 函數(shù),后者會(huì)試著從堆中 malloc 32 個(gè) 4 字節(jié)的 int,然后初始化動(dòng)態(tài)數(shù)組(如果 malloc 成功)。初始化成功后,main 函數(shù)會(huì)調(diào)用 print_ints函數(shù)。程序中并沒(méi)有調(diào)用 free 來(lái)對(duì)應(yīng) malloc 操作;因此,內(nèi)存泄漏了。

如果安裝了 valgrind 工具箱,下面的命令會(huì)檢查 leaky 程序是否存在內(nèi)存泄漏(% 是命令行提示符):

  1. % valgrind --leak-check=full ./leaky 

絕大部分輸出都在下面給出了。左邊的數(shù)字 207683 是正在執(zhí)行的 leaky 程序的進(jìn)程標(biāo)識(shí)符。這份報(bào)告給出了泄漏發(fā)生位置的詳細(xì)信息,本例中位置是在 main 函數(shù)所調(diào)用的 get_ints 函數(shù)中對(duì) malloc 的調(diào)用處。

  1. ==207683== HEAP SUMMARY: 
  2. ==207683==   in use at exit: 128 bytes in 1 blocks 
  3. ==207683==   total heap usage: 2 allocs, 1 frees, 1,152 bytes allocated 
  4. ==207683==  
  5. ==207683== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1 
  6. ==207683==   at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) 
  7. ==207683==   by 0x109186: get_ints (in /home/marty/gc/leaky) 
  8. ==207683==   by 0x109236: main (in /home/marty/gc/leaky) 
  9. ==207683==  
  10. ==207683== LEAK SUMMARY: 
  11. ==207683==   definitely lost: 128 bytes in 1 blocks 
  12. ==207683==   indirectly lost: 0 bytes in 0 blocks 
  13. ==207683==   possibly lost: 0 bytes in 0 blocks 
  14. ==207683==   still reachable: 0 bytes in 0 blocks 
  15. ==207683==   suppressed: 0 bytes in 0 blocks 

如果把 main 函數(shù)改成在對(duì) print_ints 的調(diào)用之后,再加上一個(gè)對(duì) free 的調(diào)用,valgrind 就會(huì)對(duì) leaky 程序給出一個(gè)干凈的內(nèi)存健康清單:

  1. ==218462== All heap blocks were freed -- no leaks are possible 

靜態(tài)區(qū)存儲(chǔ)

在正統(tǒng)的 C 語(yǔ)言中,函數(shù)必須在所有塊之外定義。這是一些 C 編譯器支持的特性,杜絕了在另一個(gè)函數(shù)體內(nèi)定義一個(gè)函數(shù)的可能。我舉的例子都是在所有塊之外定義的函數(shù)。這樣的函數(shù)要么是 static ,即靜態(tài)的,要么是 extern,即外部的,其中 extern 是默認(rèn)值。

C 語(yǔ)言中,以 static 或 extern 修飾的函數(shù)和變量駐留在內(nèi)存中所謂的 靜態(tài)區(qū) 中,因?yàn)樵诔绦驁?zhí)行期間該區(qū)域大小是固定不變的。這兩個(gè)存儲(chǔ)類型的語(yǔ)法非常復(fù)雜,我們應(yīng)該回顧一下。在回顧之后,會(huì)有一個(gè)完整的代碼示例來(lái)生動(dòng)展示語(yǔ)法細(xì)節(jié)。在所有塊之外定義的函數(shù)或變量默認(rèn)為 extern;因此,函數(shù)和變量要想存儲(chǔ)類型為 static ,必須顯式指定:

  1. /** file1.c: outside all blocks, five definitions  **/ 
  2. int foo(int n) { return n * 2; }     /* extern by default */ 
  3. static int bar(int n) { return n; }  /* static */ 
  4. extern int baz(int n) { return -n; } /* explicitly extern */ 
  5. int num1;        /* extern */ 
  6. static int num2; /* static */ 

extern 和 static 的區(qū)別在于作用域:extern 修飾的函數(shù)或變量可以實(shí)現(xiàn)跨文件可見(jiàn)(需要聲明)。相比之下,static 修飾的函數(shù)僅在 定義 該函數(shù)的文件中可見(jiàn),而 static 修飾的變量?jī)H在 定義 該變量的文件(或文件中的塊)中可見(jiàn):

  1. static int n1;    /* scope is the file */ 
  2. void func() { 
  3.    static int n2; /* scope is func's body */ 
  4.    ... 

如果在所有塊之外定義了 static 變量,例如上面的 n1,該變量的作用域就是定義變量的文件。無(wú)論在何處定義 static 變量,變量的存儲(chǔ)都在內(nèi)存的靜態(tài)區(qū)中。

extern 函數(shù)或變量在給定文件中的所有塊之外定義,但這樣定義的函數(shù)或變量也可以在其他文件中聲明。典型的做法是在頭文件中 聲明 這樣的函數(shù)或變量,只要需要就可以包含進(jìn)來(lái)。下面這些簡(jiǎn)短的例子闡述了這些棘手的問(wèn)題。

假設(shè) extern 函數(shù) foo 在 file1.c 中 定義,有無(wú)關(guān)鍵字 extern 效果都一樣:

  1. /** file1.c **/ 
  2. int foo(int n) { return n * 2; } /* definition has a body {...} */ 

必須在其他文件(或其中的塊)中使用顯式的 extern 聲明 此函數(shù)才能使其可見(jiàn)。以下是使 extern 函數(shù) foo 在文件 file2.c 中可見(jiàn)的聲明語(yǔ)句:

  1. /** file2.c: make function foo visible here **/ 
  2. extern int foo(int); /* declaration (no body) */ 

回想一下,函數(shù)聲明沒(méi)有用大括號(hào)括起來(lái)的主體,而函數(shù)定義會(huì)有這樣的主體。

為了便于查看,函數(shù)和變量聲明通常會(huì)放在頭文件中。準(zhǔn)備好需要聲明的源代碼文件,然后就可以 #include 相關(guān)的頭文件。下一節(jié)中的 staticProg 程序演示了這種方法。

至于 extern 的變量,規(guī)則就變得更棘手了(很抱歉增加了難度!)。任何 extern 的對(duì)象——無(wú)論函數(shù)或變量——必須 定義 在所有塊之外。此外,在所有塊之外定義的變量默認(rèn)為 extern:

  1. /** outside all blocks **/ 
  2. int n; /* defaults to extern */ 

但是,只有在變量的 定義 中顯式初始化變量時(shí),extern 才能在變量的 定義 中顯式修飾(LCTT 譯注:換言之,如果下列代碼中的 int n1; 行前加上 extern,該行就由 定義 變成了 聲明):

  1. /** file1.c: outside all blocks **/ 
  2. int n1;             /* defaults to extern, initialized by compiler to zero */ 
  3. extern int n2 = -1; /* ok, initialized explicitly */ 
  4. int n3 = 9876;      /* ok, extern by default and initialized explicitly */ 

要使在 file1.c 中定義為 extern 的變量在另一個(gè)文件(例如 file2.c)中可見(jiàn),該變量必須在 file2.c 中顯式 聲明 為 extern 并且不能初始化(初始化會(huì)將聲明轉(zhuǎn)換為定義):

  1. /** file2.c **/ 
  2. extern int n1; /* declaration of n1 defined in file1.c */ 

為了避免與 extern 變量混淆,經(jīng)驗(yàn)是在 聲明 中顯式使用 extern(必須),但不要在 定義 中使用(非必須且棘手)。對(duì)于函數(shù),extern 在定義中是可選使用的,但在聲明中是必須使用的。下一節(jié)中的 staticProg 示例會(huì)把這些點(diǎn)整合到一個(gè)完整的程序中。

staticProg 示例

staticProg 程序由三個(gè)文件組成:兩個(gè) C 語(yǔ)言源文件(static1.c 和 static2.c)以及一個(gè)頭文件(static.h),頭文件中包含兩個(gè)聲明:

  1. /** header file static.h **/ 
  2. #define NumCount 100               /* macro */ 
  3. extern int global_nums[NumCount];  /* array declaration */ 
  4. extern void fill_array();          /* function declaration */ 

兩個(gè)聲明中的 extern,一個(gè)用于數(shù)組,另一個(gè)用于函數(shù),強(qiáng)調(diào)對(duì)象在別處(“外部”)定義:數(shù)組 global_nums 在文件 static1.c 中定義(沒(méi)有顯式的 extern),函數(shù) fill_array 在文件 static2.c 中定義(也沒(méi)有顯式的 extern)。每個(gè)源文件都包含了頭文件 static.h。static1.c 文件定義了兩個(gè)駐留在內(nèi)存靜態(tài)區(qū)域中的數(shù)組(global_nums 和 more_nums)。第二個(gè)數(shù)組有 static 修飾,這將其作用域限制為定義數(shù)組的文件 (static1.c)。如前所述, extern 修飾的 global_nums 則可以實(shí)現(xiàn)在多個(gè)文件中可見(jiàn)。

  1. /** static1.c **/ 
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4. #include "static.h"             /* declarations */ 
  5. int global_nums[NumCount];      /* definition: extern (global) aggregate */ 
  6. static int more_nums[NumCount]; /* definition: scope limited to this file */ 
  7. int main() { 
  8.   fill_array(); /** defined in file static2.c **/ 
  9.   unsigned i; 
  10.   for (i = 0; i < NumCount; i++) 
  11.     more_nums[i] = i * -1; 
  12.   /* confirm initialization worked */ 
  13.   for (i = 0; i < NumCount; i += 10)  
  14.     printf("%4i\t%4i\n", global_nums[i], more_nums[i]); 
  15.      
  16.   return 0;   

下面的 static2.c 文件中定義了 fill_array 函數(shù),該函數(shù)由 main(在 static1.c 文件中)調(diào)用;fill_array 函數(shù)會(huì)給名為 global_nums 的 extern 數(shù)組中的元素賦值,該數(shù)組在文件 static1.c 中定義。使用兩個(gè)文件的唯一目的是凸顯 extern 變量或函數(shù)能夠跨文件可見(jiàn)。

  1. /** static2.c **/ 
  2. #include "static.h" /** declarations **/ 
  3. void fill_array() { /** definition **/ 
  4.   unsigned i; 
  5.   for (i = 0; i < NumCount; i++) global_nums[i] = i + 2; 

staticProg 程序可以用如下編譯:

  1. % gcc -o staticProg static1.c static2.c 

從匯編語(yǔ)言看更多細(xì)節(jié)

現(xiàn)代 C 編譯器能夠處理 C 和匯編語(yǔ)言的任意組合。編譯 C 源文件時(shí),編譯器首先將 C 代碼翻譯成匯編語(yǔ)言。這是對(duì)從上文 static1.c 文件生成的匯編語(yǔ)言進(jìn)行保存的命令:

  1. % gcc -S static1.c 

生成的文件就是 static1.s。這是文件頂部的一段代碼,額外添加了行號(hào)以提高可讀性:

  1.     .file    "static1.c"          ## line  1 
  2.     .text                         ## line  2 
  3.     .comm    global_nums,400,32   ## line  3 
  4.     .local    more_nums           ## line  4 
  5.     .comm    more_nums,400,32     ## line  5 
  6.     .section    .rodata           ## line  6 
  7. .LC0:                             ## line  7 
  8.     .string    "%4i\t%4i\n"       ## line  8 
  9.     .text                         ## line  9 
  10.     .globl    main                ## line 10 
  11.     .type    main, @function      ## line 11 
  12. main:                             ## line 12 
  13. ... 

諸如 .file(第 1 行)之類的匯編語(yǔ)言指令以句點(diǎn)開(kāi)頭。顧名思義,指令會(huì)指導(dǎo)匯編程序?qū)R編語(yǔ)言翻譯成機(jī)器代碼。.rodata 指令(第 6 行)表示后面是只讀對(duì)象,包括字符串常量 "%4i\t%4i\n"(第 8 行),main 函數(shù)(第 12 行)會(huì)使用此字符串常量來(lái)實(shí)現(xiàn)格式化輸出。作為標(biāo)簽引入(通過(guò)末尾的冒號(hào)實(shí)現(xiàn))的 main 函數(shù)(第 12 行),同樣也是只讀的。

在匯編語(yǔ)言中,標(biāo)簽就是地址。標(biāo)簽 main:(第 12 行)標(biāo)記了 main 函數(shù)代碼開(kāi)始的地址,標(biāo)簽 .LC0:(第 7 行)標(biāo)記了格式化字符串開(kāi)頭所在的地址。

global_nums(第 3 行)和 more_nums(第 4 行)數(shù)組的定義包含了兩個(gè)數(shù)字:400 是每個(gè)數(shù)組中的總字節(jié)數(shù),32 是每個(gè)數(shù)組(含 100 個(gè) int 元素)中每個(gè)元素的比特?cái)?shù)。(第 5 行中的 .comm 指令表示 common name,可以忽略。)

兩個(gè)數(shù)組定義的不同之處在于 more_nums 被標(biāo)記為 .local(第 4 行),這意味著其作用域僅限于其所在文件 static1.s。相比之下,global_nums 數(shù)組就能在多個(gè)文件中實(shí)現(xiàn)可見(jiàn),包括由 static1.c 和 static2.c 文件翻譯成的匯編文件。

最后,.text 指令在匯編代碼段中出現(xiàn)了兩次(第 2 行和第 9 行)。術(shù)語(yǔ)“text”表示“只讀”,但也會(huì)涵蓋一些讀/寫變量,例如兩個(gè)數(shù)組中的元素。盡管本文展示的匯編語(yǔ)言是針對(duì) Intel 架構(gòu)的,但 Arm6 匯編也非常相似。對(duì)于這兩種架構(gòu),.text 區(qū)域中的變量(本例中為兩個(gè)數(shù)組中的元素)會(huì)自動(dòng)初始化為零。

總結(jié)

C 語(yǔ)言中的內(nèi)存高效和內(nèi)存安全編程準(zhǔn)則很容易說(shuō)明,但可能會(huì)很難遵循,尤其是在調(diào)用設(shè)計(jì)不佳的庫(kù)的時(shí)候。準(zhǔn)則如下:

  • 盡可能使用棧存儲(chǔ),進(jìn)而鼓勵(lì)編譯器將通用寄存器用作暫存器,實(shí)現(xiàn)優(yōu)化。棧存儲(chǔ)代表了高效的內(nèi)存使用并促進(jìn)了代碼的整潔和模塊化。永遠(yuǎn)不要返回指向基于棧的存儲(chǔ)的指針。
  • 小心使用堆存儲(chǔ)。C(和 C++)中的重難點(diǎn)是確保動(dòng)態(tài)分配的存儲(chǔ)盡快解除分配。良好的編程習(xí)慣和工具(如 valgrind)有助于攻關(guān)這些重難點(diǎn)。優(yōu)先選用自身提供釋放函數(shù)的庫(kù),例如 nestedHeap 代碼示例中的 free_all 釋放函數(shù)。
  • 謹(jǐn)慎使用靜態(tài)存儲(chǔ),因?yàn)檫@種存儲(chǔ)會(huì)自始至終地影響進(jìn)程的內(nèi)存占用。特別是盡量避免使用 extern 和 static 數(shù)組。

 

責(zé)任編輯:未麗燕 來(lái)源: Linux中國(guó)
相關(guān)推薦

2011-04-25 14:06:23

java

2009-07-06 13:38:02

2015-12-28 11:41:57

JVM內(nèi)存區(qū)域內(nèi)存溢出

2009-11-05 09:42:42

Visual Stud

2009-06-08 16:52:00

2009-09-24 13:22:58

Nhibernate代碼生成

2010-06-30 09:07:09

UML建模分析

2010-06-17 22:22:24

2015-09-16 15:21:23

Android性能優(yōu)化內(nèi)存

2016-12-22 17:21:11

Android性能優(yōu)化內(nèi)存泄漏

2011-08-02 10:50:56

iOS開(kāi)發(fā) 內(nèi)存緩存

2012-02-01 13:57:40

內(nèi)存緩存機(jī)制

2009-08-26 14:52:19

.NET Framew

2020-10-26 10:58:39

Volatility的

2009-06-27 10:59:04

2009-11-30 17:40:05

juniper路由

2019-04-04 11:55:59

2009-08-17 15:34:58

C#創(chuàng)建XML

2010-01-14 16:54:56

VB.NET Impo

2010-06-03 18:22:38

Hadoop
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 99精品国产一区二区三区 | 国产一级免费视频 | 极品一区 | 天天宗合网 | 色婷婷久久久久swag精品 | 国产激情视频在线 | www.狠狠操| 久久精品91久久久久久再现 | 日韩在线视频精品 | 成人性生交大片 | 久久久爽爽爽美女图片 | 国内自拍视频在线观看 | 一区二区中文字幕 | 成人在线观看免费 | 久久99精品久久久久久青青日本 | 国产精品视频网 | 天天插天天操 | 精品久久久久久久久久久久 | 欧美日韩精品一区二区 | 亚洲欧美在线一区 | 久久久久国产精品一区二区 | 日韩久久综合网 | 国产精品视频久久 | 日本午夜网站 | 国产在线一区二区三区 | 久久亚洲视频 | 亚洲国产成人精品久久久国产成人一区 | 国产精品久久久久aaaa九色 | 久久久久久高潮国产精品视 | 国产视频福利一区 | 欧美成人一区二区 | 亚洲色图第一页 | 91精品国产一区二区三区蜜臀 | 刘亦菲国产毛片bd | 国产大片黄色 | 在线国产中文字幕 | 91亚洲精品久久久电影 | 日韩中文字幕一区二区 | 成人精品久久日伦片大全免费 | 国产精品一卡二卡三卡 | 日韩欧美精品在线 |