Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 最近中文字幕无免费视频,国产亚洲视频网站,亚洲欧美日韩国产精品

          整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          不同視角理解嵌入式開發中的“數據結構”~

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效

          【說在前面的話】

          2022年了,想必已經不會有人對嵌入式開發中“數據結構(Data )”的作用產生疑問了吧?無論你是否心存疑惑,本文都將給你一個完全不同的視角。

          每每說起數據結構,很多人腦海里復現的一定是以下的內容:

          數據結構其實不是一個高大上的名詞,它意外的非常樸實——你也許每天都在用。作為一個新坑,我將在【非常C結構】系列文章中為大家分享很多嵌入式開發中很多“非?!倍帧昂糜谩钡臄祿Y構。

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效

          【人人都可以學會的“表格”】

          你不必學過所謂的“關系數據庫”也可以理解“表格(Table)”這種數據結構的本質含義。

          在C語言環境中,表格的本質就是結構體數組,即:由結構體組成的數組。這里:

          在嵌入式系統中,表格具有以下特點:

          如果一個需求能夠1)接受上述的特點;或者2)本身就具有上述特點;或者3)部分內容經過改造后可以接受上述特點——那么,就可以使用表格來保存數據了。

          一個典型的例子就是:交互菜單。

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效

          很容易看到,每一級菜單本質上都“可以”是一個表格。

          雖然在很多UI設計工具中(比如LVGL),菜單的內容是在運行時刻動態生成的(用鏈表來實現),但在嵌入式系統中,動態生成表格本身并不是一個“必須使用”的特性,相反,由于產品很多時候功能固定——菜單的內容也是固定的,因此完全沒有必要在運行時刻進行動態生成——這就滿足了表格的“在編譯時刻初始化”的要求。

          采用表格的形式來保存菜單,就獲得了在ROM中保存數據、減少RAM消耗的的優勢。同時,數組的訪問形式又進一步簡化了用戶代碼。

          另外一個常見用到表格的例子是消息地圖(Message Map),它在通信協議棧解析類的應用中非常常見,在很多結構緊湊功能復雜的中也充當著重要的角色。

          如果你較真起來,菜單也不過消息地圖的一種。表格不是實現消息地圖的唯一方式,但卻是最簡單、最常用、數據存儲密度最高的形式。在后續的例子中,我們就以“消息地圖”為例,深入聊聊表格的使用和優化。

          【表格的定義】

          一般來說,表格由兩部分構成:

          因此,表格的定義也分為兩個部分:

          記錄的定義一般格式如下:

          typedef?struct?<表格名稱>_item_t  <表格名稱>_item_t;
          struct?<表格名稱>_item_t {????//?每條記錄中的內容};

          這里,第一行的typedef所在行的作用是“前置聲明”;struct所在行的作用是定義結構體的實際內容。雖然我們完全可以將“前置聲明”和“結構體定義”合二為一,寫作:

          typedef?struct <表格名稱>_item_t {    // 每條記錄中的內容} <表格名稱>_item_t;

          但基于以下原因,我們還是推薦大家堅持第一種寫法:

          以消息地圖為例,一個常見的記錄結構體定義如下:

          typedef?struct?msg_item_t msg_item_t;
          struct?msg_item_t { uint8_t chID; //!< 指令????uint8_t?chAccess;?????????????//!????uint16_t hwValidDataSize; //!< 數據長度要求????bool?(*fnHandler)(msg_item_t?*ptMSG,??????? void?*pData,???????????????????????uint_fast16_t?hwSize);};

          在這個例子中,我們腦補了一個通信指令系統,當我們通過通信前端進行數據幀解析后,獲得了以下的內容:

          為了方便指令解析,我們也需要有針對性的來設計每一條指令的內容,因此,我們加入了chID來存儲指令碼;并加入了函數指針來為當前指令綁定一個處理函數;考慮到每條指令所需的最小有效數據長度是已知的,因此,我們通過來記錄這一信息,以便進行信息檢索時快速的做出判斷。具體如何使用,我們后面再說。

          對表格來說,容器是所有記錄的容身之所,可以簡單,但不可以缺席。最簡單的容器就是數組,例如:

          const?msg_item_t?c_tMSGTable[20];

          這里,類型的數組就是表格的容器,而且我們手動規定了數組中元素的個數。實踐中,我們通常不會像這樣手動的“限定”表格中元素的個數,而是直接“偷懶”——埋頭初始化數組,然后讓編譯器替我們去數數——根據我們初始化元素的個數來確定數組的元素數量,例如:

          const msg_item_t c_tMSGTable[] = {    [0] = {????????.chID?=?0,????????.fnHandler = NULL,    },    [1] = {        ...    },    ...};

          上述寫法是C99語法,不熟悉的小伙伴可以再去翻翻語法書哦。說句題外話,2022年了,連頑固不化的Linux都擁抱C11了,不要再抱著C89規范不放了,起碼用個C99沒問題的。

          上面寫法的好處主要是方便我們偷懶,減少不必要的“數數”過程。那么,我們要如何知道一個表格中數組究竟有多少個元素呢?別慌,我們有sizeof():

          #ifndef dimof#???dimof(__array)     (sizeof(__array)/sizeof(__array[0]))#endif

          這個語法糖dimof()可不是我發明的,不信你問Linux。它的原理很簡單,當我們把數組名稱傳給dimof()時,它會:

          通過 sizeof() 來獲取整個目標數組的字節尺寸;

          通過 sizeof([0]) 來獲取數組第一個元素的字節尺寸——也就是數組元素的尺寸;

          通過除法獲取數組中元素的個數。

          【表格的訪問(遍歷)】

          由于表格的本質是結構體數組,因此,針對表格最常見的操作就是遍歷(搜索)了。還以前面消息地圖為例子:

          static?volatile?uint8_t?s_chCurrentAccessPermission;
          /*!?\brief?搜索消息地圖,并執行對應的處理程序?*!?\retval?false??消息不存在或者消息處理函數覺得內容無效?*! \retval true 消息得到了正確的處理?*/bool?search_msgmap(uint_fast8_t?chID,?????????????????? void *pData,?????????????????? uint_fast16_t hwSize){????for?(int?n?=?0;?n?????????msg_item_t *ptItem = &c_tMSGTable[n];????????if?(chID?!=?ptItem->chID) {???????? continue;????????}????????if?(!(ptItem->chAccess & s_chCurrentAccessPermission)) {????????????continue;??//!????????}????????if?(hwSize < ptItem->hwSize) {????????????continue; //!< 數據太小了????????}????????if (NULL == ptItem->fnHandler) {????????????continue;??//!????????}????????????????//! 調用消息處理函數????????return?ptItem->fnHandler(ptItem,?pData,?hwSize);????}????????return?false;???//!}

          別看這個函數“很有料”的樣子,其本質其實特別簡單:

          其實上述代碼隱藏了一個特性:就是這個例子中的消息地圖中允許出現chID相同的消息的——這里的技巧是:對同一個chID值的消息,我們可以針對不同的訪問權限(值)來提供不同的處理函數。比如,通信系統中,我們可以設計多種權限和模式,比如:只讀模式、只寫模式、安全模式等等。不同模式對應不同的值。這樣,對哪怕同樣的指令,我們也可以根據當前模式的不同提供不同的處理函數——這只是一種思路,供大家參考。

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效

          【由多實例引入的問題】

          前面的例子為我們展示表格使用的大體細節,對很多嵌入式應用場景來說,已經完全夠用了。但愛思考的小伙伴一定已經發現了問題:

          如果我的系統中有多個消息地圖(每個消息地圖中消息數量是不同的),我改怎么復用代碼呢?

          指定的實例無效_安裝數據庫指定的實例名稱無效_安裝數據庫指定的實例名稱無效

          為了照顧還一臉懵逼的小伙伴,我把這個問題給大家翻譯翻譯:

          簡而言之,()現在跟某一個消息地圖(數組)綁定死了,如果要讓它支持其它的消息地圖(其它數組),就必須想辦法將其與特定的數組解耦,換句話說,在使用()的時候,要提供目標的消息地圖的指針,以及消息地圖中元素的個數。

          一個頭疼醫頭腳疼醫腳的修改方案呼之欲出:

          bool?search_msgmap(msg_item_t?*ptMSGTable,                   uint_fast16_t hwCount,                   uint_fast8_t chID,                   void *pData,                   uint_fast16_t hwSize){    for (int n = 0; n < hwCount; n++) {        msg_item_t *ptItem = &ptMSGTable[n];        if (chID != ptItem->chID) {            continue;        }        ...
          //! 調用消息處理函數 return ptItem->fnHandler(ptItem, pData, hwSize); }
          return false; //!< 沒找到對應的消息}

          假設我們有多個消息地圖,對應不同的工作模式:

          const?msg_item_t?c_tMSGTableUserMode[] = {    ...};const msg_item_t c_tMSGTableSetupMode[] = {    ...};
          const msg_item_t c_tMSGTableDebugMode[] = { ...};
          const msg_item_t c_tMSGTableFactoryMode[] = { ...};

          在使用的時候,可以這樣:

          typedef enum?{????USER_MODE?=?0,????//!????SETUP_MODE,???????//!????DEBUG_MODE,???????//!????FACTORY_MODE,?????//!}?comm_mode_t;
          bool?frame_process_backend(comm_mode_t tWorkMode,?????????????????????????? uint_fast8_t chID,?????????????????????????? void *pData,?????????????????????????? uint_fast16_t hwSize){????bool?bHandled = false; switch (tWorkMode) { case USER_MODE:????????????bHandled = search_msgmap(???????????? c_tMSGTableUserMode, ???????????? dimof(c_tMSGTableUserMode),???????????? chID,???????????? pData,??????????????????????????hwSize);????????????break;???????? case SETUP_MODE: bHandled = search_msgmap( c_tMSGTableSetupMode, dimof(c_tMSGTableUserMode), chID, pData, hwSize); break;?????????... }
          return bHandled;}

          看起來很不錯,對吧?非也非也!早得很呢。

          安裝數據庫指定的實例名稱無效_安裝數據庫指定的實例名稱無效_指定的實例無效

          【表格定義的完全體】

          前面我們說過,表格的定義分兩個部分:

          其中,關于容器的定義,我們說過,數組是容器的最簡單形式。那么容器定義的完全體是怎樣的呢?

          安裝數據庫指定的實例名稱無效_安裝數據庫指定的實例名稱無效_指定的實例無效

          “還是結構體”!

          是的,表格條目的本質是結構體,表格容器的本質也是一個結構體:

          typedef struct <表格名稱>_item_t  <表格名稱>_item_t;
          struct <表格名稱>_item_t { // 每條記錄中的內容};
          typedef struct <表格名稱>_t <表格名稱>_t;
          struct <表格名稱>_t {????uint16_t?hwItemSize;????uint16_t?hwCount;????<表格名稱>_item_t *ptItems;};

          容易發現,這里表格容器被定義成了一個叫做 _t 的結構體,其中包含了三個至關重要的元素:

          這個其實是來湊數的,因為32位系統中指針4字節對齊的緣故,2字節的hwCount橫豎會產生2字節的氣泡。不理解這一點的小伙伴,可以參考文章《》

          還是以前面消息地圖為例,我們來看看新的容器應該如何定義和使用:

          typedef?struct?msg_item_t?msg_item_t;
          struct msg_item_t { uint8_t chID; //!< 指令 uint8_t chAccess; //!< 訪問權限檢測 uint16_t hwValidDataSize; //!< 數據長度要求 bool (*fnHandler)(msg_item_t *ptMSG, void *pData, uint_fast16_t hwSize);};
          typedef?struct?msgmap_t?msgmap_t;
          struct msgmap_t { uint16_t hwItemSize; uint16_t hwCount;????msg_item_t *ptItems;};
          const msg_item_t c_tMSGTableUserMode[] = { ...};
          const?msgmap_t?c_tMSGMapUserMode = {????.hwItemSize?=?sizeof(msg_item_t),????.hwCount?=?dimof(c_tMSGTableUserMode),????.ptItems = c_tMSGTableUserMode,};

          既然有了定義,()也要做相應的更新:

          bool?search_msgmap(msgmap_t?*ptMSGMap,                   uint_fast8_t chID,                   void *pData,                   uint_fast16_t hwSize){    for (int n = 0; n < ptMSGMap->hwCount; n++) {        msg_item_t *ptItem = &(ptMSGMap->ptItems[n]);        if (chID != ptItem->chID) {            continue;        }        ...
          //! 調用消息處理函數 return ptItem->fnHandler(ptItem, pData, hwSize); }
          return false; //!< 沒找到對應的消息}

          看到這里,相信很多小伙伴內心是毫無波瀾的……

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效

          “是的……是稍微優雅一點……然后呢?”

          “就這???就這?!”

          別急,下面才是見證奇跡的時刻。

          【要優雅……】

          在前面的例子中,我們注意到表格的初始化是分兩部分進行的:

          const msg_item_t c_tMSGTableUserMode[] = {    [0] = {        .chID = 0,        .fnHandler = NULL,    },    [1] = {        ...    },    ...};
          const?msgmap_t?c_tMSGMapUserMode = {????.hwItemSize?=?sizeof(msg_item_t),????.hwCount?=?dimof(c_tMSGTableUserMode),????.ptItems = c_tMSGTableUserMode,};

          那么,我們可不可以把它們合二為一呢?這樣:

          要做到這一點,我們可以使用一個類似“匿名數組”的功能:

          我們想象中的樣子:

          const msgmap_t c_tMSGMapUserMode = {    .hwItemSize = sizeof(msg_item_t),    .hwCount = dimof(c_tMSGTableUserMode),    .ptItems = const msg_item_t c_tMSGTableUserMode[] = {          [0] = {              .chID = 0,              .fnHandler = NULL,          },          [1] = {              ...          },          ...      },};

          使用“匿名數組”后的樣子(也就是刪除數組名稱后的樣子):

          const msgmap_t c_tMSGMapUserMode = {    .hwItemSize = sizeof(msg_item_t),    .hwCount = dimof(c_tMSGTableUserMode),????.ptItems?=?(msg_item_t?[]){          [0] = {              .chID = 0,              .fnHandler = NULL,          },          [1] = {              ...          },          ...      },};

          指定的實例無效_安裝數據庫指定的實例名稱無效_安裝數據庫指定的實例名稱無效

          其實,這不是什么“黑魔法”,而是一個廣為使用的GNU擴展,被稱為“復合式描述( literal)”,本質上就是一種以“省略”數組或結構體名稱的方式來初始化數組或結構體的語法結構。具體語法介紹,小伙伴們可以參考這篇文章《》。

          安裝數據庫指定的實例名稱無效_安裝數據庫指定的實例名稱無效_指定的實例無效

          眼尖的小伙伴也許已經發現了問題:既然我們省略了變量名,那么如何通過 dimof() 來獲取數組元素的個數呢?

          安裝數據庫指定的實例名稱無效_安裝數據庫指定的實例名稱無效_指定的實例無效

          少俠好眼力!

          解決方法不僅有,而且簡單粗暴:

          const msgmap_t c_tMSGMapUserMode = {    .hwItemSize = sizeof(msg_item_t),
          .hwCount = dimof((msg_item_t []){ [0] = { .chID = 0, .fnHandler = NULL, }, [1] = { ... }, ...??????}),
          ????.ptItems?=?(msg_item_t?[]){ [0] = { .chID = 0, .fnHandler = NULL, }, [1] = { ... }, ... },};

          指定的實例無效_安裝數據庫指定的實例名稱無效_安裝數據庫指定的實例名稱無效

          所以說?……

          為了優雅的初始化……

          我們要把同樣的內容寫兩次???!

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效

          手寫的確挺愚蠢,但宏可以啊!

          #define __impl_table(__item_type, ...)                   \????.ptItems?=?(__item_type?[])?{??????????????????????? \        __VA_ARGS__                                      \    },                                                   \????.hwCount?=?sizeof((__item_type?[])?{?__VA_ARGS__?})??\             / sizeof(__item_type),                      \    .hwItemSize = sizeof(__item_type)
          #define?impl_table(__item_type,?...)???? \ __impl_table(__item_type,?__VA_ARGS__)

          借助上面的語法糖,我們可以輕松的將整個表格的初始化變得簡單優雅:

          const msgmap_t c_tMSGMapUserMode = {    impl_table(msg_item_t,           [0] = {              .chID = 0,              .fnHandler = NULL,          },          [1] = {              ...          },          ...    ),};

          這下舒服了吧?

          【禁止套娃……】

          還記得前面多實例的例子吧?

          const msg_item_t c_tMSGTableUserMode[] = {    ...};const msg_item_t c_tMSGTableSetupMode[] = {    ...};
          const msg_item_t c_tMSGTableDebugMode[] = { ...};
          const msg_item_t c_tMSGTableFactoryMode[] = { ...};

          現在當然就要改為如下的形式了:

          const msgmap_t c_tMSGMapUserMode = {    impl_table(msg_item_t,         ...????),};
          const msgmap_t c_tMSGMapSetupMode = { impl_table(msg_item_t, ... ),};
          const msgmap_t c_tMSGMapDebugMode = { impl_table(msg_item_t, ... ),};
          const msgmap_t c_tMSGMapFactoryMode = { impl_table(msg_item_t, ... ),};

          但……它們不都是類型的么?為啥不做一個數組呢?

          typedef enum {    USER_MODE = 0,    //!< 普通的用戶模式    SETUP_MODE,       //!< 出廠后的安裝模式    DEBUG_MODE,       //!< 工程師專用的調試模式    FACTORY_MODE,     //!< 最高權限的工廠模式} comm_mode_t;
          const?msgmap_t?c_tMSGMap[] = { [USER_MODE] = { impl_table(msg_item_t, ... ), }, [SETUP_MODE] = { impl_table(msg_item_t, ... ), }, [DEBUG_MODE] = { impl_table(msg_item_t, ... ), }, [FACTORY_MODE] = { impl_table(msg_item_t, ... ), },};

          是不是有點意思了?再進一步,我們完全可以做一個新的表格,表格的元素就是 呀?

          typedef?struct?cmd_modes_t?cmd_modes_t;
          struct cmd_modes_t { uint16_t hwItemSize; uint16_t hwCount; msgmap_t *ptItems;};

          然后就可以開始套娃咯:

          const?cmd_modes_t?c_tCMDModes?=?{????impl_table(msgmap_t,????    [USER_MODE] = {            impl_table(msg_item_t,                 [0] = {                    .chID = 0,                    .fnHandler = NULL,                },                [1] = {                    ...                },                ...            ),        },        [SETUP_MODE] = {            impl_table(msg_item_t,                 ...            ),        },        [DEBUG_MODE] = {            impl_table(msg_item_t,                 ...            ),        },        [FACTORY_MODE] = {            impl_table(msg_item_t,                 ...            ),        },????),};

          安裝數據庫指定的實例名稱無效_安裝數據庫指定的實例名稱無效_指定的實例無效

          【差異化……】

          在前面的例子中,我們可以根據新的定義方式更新函數d()函數:


          extern const cmd_modes_t c_tCMDModes;
          bool frame_process_backend(comm_mode_t tWorkMode, uint_fast8_t chID, void *pData, uint_fast16_t hwSize){ bool bHandled = false;
          ????if?(tWorkMode?> FACTORY_MODE) {???? return false;????}????????return search_msgmap( &(c_tCMDModes.ptItems[tWorkMode]),? chID, pData,??????????????????????????hwSize);}

          是不是特別優雅?

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效

          把容器定義成結構體還有一個好處,就是可以給表格更多的差異化,這意味著,除了條目數組相關的內容外,我們還可以放入其它東西,比如:

          現有的d()為每一個消息地圖()都使用相同的處理函數(),這顯然缺乏差異化的可能性。如果每個消息地圖都有可能有自己的特殊處理函數怎么辦呢?

          為了實現這一功能,我們可以對 進行擴展:

          typedef struct msgmap_t msgmap_t;
          struct msgmap_t { uint16_t hwItemSize; uint16_t hwCount; msg_item_t *ptItems;????bool?(*fnHandler)(msgmap_t?*ptMSGMap, uint_fast8_t chID, void *pData,??????????????????????uint_fast16_t?hwSize);};

          則初始化的時候,我們就可以給每個消息地圖指定一個不同的處理函數:

          extern     bool?msgmap_user_mode_handler(msgmap_t?*ptMSGMap,                      uint_fast8_t chID,                      void *pData,                      uint_fast16_t hwSize);
          extern bool msgmap_debug_mode_handler(msgmap_t *ptMSGMap, uint_fast8_t chID, void *pData, uint_fast16_t hwSize);
          const cmd_modes_t c_tCMDModes = { impl_table(msgmap_t, [USER_MODE] = { impl_table(msg_item_t, ????????????????... ), .fnHandler = &msgmap_user_mode_handler, }, [SETUP_MODE] = { impl_table(msg_item_t, ... ),????????????.fnHandler?=?NULL;?//! }, [DEBUG_MODE] = { impl_table(msg_item_t, ... ), .fnHandler = &msgmap_debug_mode_handler, }, [FACTORY_MODE] = { impl_table(msg_item_t, ... ),????????????//.fnHandler = NULL 什么都不寫,就是NULL(0) }, ),};

          此時,我們再更新d()函數,讓上述差異化功能成為可能:


          bool frame_process_backend(comm_mode_t tWorkMode, uint_fast8_t chID, void *pData, uint_fast16_t hwSize){ bool bHandled = false;????msgmap_t?*ptMSGMap?= c_tCMDModes.ptItems[tWorkMode];????if?(tWorkMode?> FACTORY_MODE) {???? return false;????}????????//!?調用每個消息地圖自己的處理程序????if (NULL != ptMSGMap->fnHandler) {?????????return?ptMSGMap->fnHandler(ptMSGMap,? chID, pData, hwSize);????}????//!?默認的消息地圖處理程序????return search_msgmap( ptMSGMap, chID, pData, hwSize);}

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效

          【說在后面的話】

          啥都不說了……你們看著辦吧。我們下期再見。

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效

          安裝數據庫指定的實例名稱無效_指定的實例無效_安裝數據庫指定的實例名稱無效


          主站蜘蛛池模板: 亚洲大尺度无码无码专线一区| 亚洲熟妇成人精品一区| 国产精品一区三区| 久久中文字幕一区二区| 精品欧美一区二区在线观看 | 国产在线精品观看一区| 清纯唯美经典一区二区| 日韩电影在线观看第一区| 国产自产对白一区| 精品无码一区二区三区爱欲九九 | 国产日韩一区二区三区在线观看| 国产精品一区二区久久沈樵| 久久人妻无码一区二区| 中文字幕一区二区三区精彩视频 | 亚洲色欲一区二区三区在线观看 | 亚洲一区二区三区四区在线观看| 久久久99精品一区二区| 在线电影一区二区三区| 无码视频一区二区三区| 爆乳无码AV一区二区三区 | 国产成人高清视频一区二区| 高清一区二区在线观看| 国产中的精品一区的| 国产一区二区三区在线电影| 国产一区二区三区不卡在线观看 | 亚洲AV无码一区二区三区性色 | 日本一区二区三区久久| 国产精品无码一区二区三区在| 国精产品999一区二区三区有限| 多人伦精品一区二区三区视频| 国产香蕉一区二区三区在线视频 | 色婷婷香蕉在线一区二区| 无码夜色一区二区三区| 国产成人高清亚洲一区91| 精品在线一区二区| 国产亚洲一区区二区在线| 亚洲AV无码一区二区二三区软件| 色欲综合一区二区三区| 中文字幕日韩欧美一区二区三区| 国产欧美一区二区精品仙草咪| 精品天海翼一区二区|