色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

編寫優質嵌入式C程序的基礎

STM32嵌入式開發 ? 來源:STM32嵌入式開發 ? 作者:STM32嵌入式開發 ? 2022-10-20 09:43 ? 次閱讀

本文面向的,正是使用單片機ARM7、Cortex-M3這類微控制器編程人員。

C語言詭異且有種種陷阱和缺陷,需要程序員多年歷練才能達到較為完善的地步。

總是有大批的初學者,前仆后繼的倒在這些陷阱和缺陷上,民用設備、工業設備甚至是航天設備都不例外。本文將結合具體例子再次審視它們,希望引起足夠重視。深入理解C語言特性,是編寫優質嵌入式C程序的基礎。

由于篇幅限制,后續再推送編譯器、防御性編程、測試和編程思想這幾個方面的內容,來討論如何編寫優質嵌入式C程序。

1 處處都是陷阱

1.1 無心之過

1) “=”和”==”

將比較運算符”==”誤寫成賦值運算符”=”,可能是絕大多數人都遇到過的,比如下面代碼:

if(x=5)
{
//其它代碼   
}

代碼的本意是比較變量x是否等于常量5,但是誤將”==”寫成了”=”,if語句恒為真。如果在邏輯判斷表達式中出現賦值運算符,現在的大多數編譯器會給出警告信息。比如keil MDK會給出警告提示:“warning: #187-D: use of "=" where"==" may have been intended”,但并非所有程序員都會注意到這類警告,因此有經驗的程序員使用下面的代碼來避免此類錯誤:

if(5==x)
{
//其它代碼
}

將常量放在變量x的左邊,即使程序員誤將’==’寫成了’=’,編譯器會產生一個任誰也不能無視的語法錯誤信息:不可給常量賦值!

2) 復合賦值運算符

復合賦值運算符(+=、*=等等)雖然可以使表達式更加簡潔并有可能產生更高效的機器代碼,但某些復合賦值運算符也會給程序帶來隱含Bug,比如”+=”容易誤寫成”=+”,代碼如下:

tmp=+1;

代碼本意是想表達tmp=tmp+1,但是將復合賦值運算符”+=”誤寫成”=+”:將正整數常量1賦值給變量tmp。編譯器會欣然接受這類代碼,連警告都不會產生。

如果你能在調試階段就發現這個Bug,真應該慶祝一下,否則這很可能會成為一個重大隱含Bug,且不易被察覺。

復合賦值運算符”-=”也有類似問題存在。

3) 其它容易誤寫

  • 使用了中文標點
  • 頭文件聲明語句最后忘記結束分號
  • 邏輯與&&和位與&、邏輯或||和位或|、邏輯非!和位取反~
  • 字母l和數字1、字母O和數字0

這些誤寫其實容易被編譯器檢測出,只需要關注編譯器對此的提示信息,就能很快解決。

1.2 數組下標

數組常常也是引起程序不穩定的重要因素,C語言數組的迷惑性與數組下標從0開始密不可分,你可以定義int test[30],但是你絕不可以使用數組元素test [30],除非你自己明確知道在做什么。

1.3 容易被忽略的break關鍵字

1) 不能漏加的break

switch…case語句可以很方便的實現多分支結構,但要注意在合適的位置添加break關鍵字。程序員往往容易漏加break從而引起順序執行多個case語句,這也許是C的一個缺陷之處。

對于switch…case語句,從概率論上說,絕大多數程序一次只需執行一個匹配的case語句,而每一個這樣的case語句后都必須跟一個break。去復雜化大概率事件,這多少有些不合常情。

2) 不能亂加的break

break關鍵字用于跳出最近的那層循環語句或者switch語句,但程序員往往不夠重視這一點。

1990年1月15日,AT&T電話網絡位于紐約的一臺交換機宕機并且重啟,引起它鄰近交換機癱瘓,由此及彼,一個連著一個,很快,114型交換機每六秒宕機重啟一次,六萬人九小時內不能打長途電話。

當時的解決方式:工程師重裝了以前的軟件版本。。。事后的事故調查發現,這是break關鍵字誤用造成的。《C專家編程》提供了一個簡化版的問題源碼:

028ea2d0-5017-11ed-a3b6-dac502259ad0.png

那個程序員希望從if語句跳出,但他卻忘記了break關鍵字實際上跳出最近的那層循環語句或者switch語句。現在它跳出了switch語句,執行了use_modes_pointer()函數。但必要的初始化工作并未完成,為將來程序的失敗埋下了伏筆。

1.4 意想不到的八進制

將一個整形常量賦值給變量,代碼如下所示:

int a=34, b=034;

變量a和b相等嗎?

答案是不相等的。我們知道,16進制常量以’0x’為前綴,10進制常量不需要前綴,那么8進制呢?它與10進制和16進制表示方法都不相同,它以數字’0’為前綴,這多少有點奇葩:三種進制的表示方法完全不相同。

如果8進制也像16進制那樣以數字和字母表示前綴的話,或許更有利于減少軟件Bug,畢竟你使用8進制的次數可能都不會有誤使用的次數多!下面展示一個誤用8進制的例子,最后一個數組元素賦值錯誤:

a[0]=106;       /*十進制數106*/
 a[1]=112;      /*十進制數112*/
 a[2]=052;       /*實際為十進制數42,本意為十進制52*/

1.5指針加減運算

**指針的加減運算是特殊的。**下面的代碼運行在32位ARM架構上,執行之后,a和p的值分別是多少?

int a=1;
int *p=(int *)0x00001000;
a=a+1;
p=p+1;

對于a的值很容判斷出結果為2,但是p的結果卻是0x00001004。指針p加1后,p的值增加了4,這是為什么呢?原因是指針做加減運算時是以指針的數據類型為單位。p+1實際上是按照公式p+1*sizeof(int)來計算的。不理解這一點,在使用指針直接操作數據時極易犯錯。

某項目使用下面代碼對連續RAM初始化零操作,但運行發現有些RAM并沒有被真正清零。

032365dc-5017-11ed-a3b6-dac502259ad0.png

通過分析我們發現,由于pRAMaddr是一個無符號int型指針變量,所以pRAMaddr+=4代碼其實使pRAMaddr偏移了4*sizeof(int)=16個字節,所以每執行一次for循環,會使變量pRAMaddr偏移16個字節空間,但只有4字節空間被初始化為零。其它的12字節數據的內容,在大多數架構處理器中都會是隨機數。

1.6關鍵字sizeof

不知道有多少人最初認為sizeof是一個函數。其實它是一個關鍵字,其作用是返回一個對象或者類型所占的內存字節數,對絕大多數編譯器而言,返回值為無符號整形數據。需要注意的是,使用sizeof獲取數組長度時,不要對指針應用sizeof操作符,比如下面的例子:

void ClearRAM(char array[])
{
int i ;
for(i=0;i<sizeof(array)/sizeof(array[0]);i++)//這里用法錯誤,array實際上是指針
     {
array[i]=0x00;
     }
 }


int main(void)
{
char Fle[20];


     ClearRAM(Fle);          //只能清除數組Fle中的前四個元素  
 }

我們知道,對于一個數組array[20],我們使用代碼sizeof(array)/sizeof(array[0])可以獲得數組的元素(這里為20),但數組名和指針往往是容易混淆的,有且只有一種情況下數組名是可以當做指針的,那就是**數組名作為函數形參時,數組名被認為是指針,同時,它不能再兼任數組名。

**注意只有這種情況下,數組名才可以當做指針,但不幸的是這種情況下容易引發風險。在ClearRAM函數內,作為形參的array[]不再是數組名了,而成了指針。sizeof(array)相當于求指針變量占用的字節數,在32位系統下,該值為4,sizeof(array)/sizeof(array[0])的運算結果也為4。所以在main函數中調用ClearRAM(Fle),也只能清除數組Fle中的前四個元素了。

1.7增量運算符’++’和減量運算符‘--‘

增量運算符”++”和減量運算符”--“既可以做前綴也可以做后綴。**前綴和后綴的區別在于值的增加或減少這一動作發生的時間是不同的。**作為前綴是先自加或自減然后做別的運算,作為后綴時,是先做運算,之后再自加或自減。許多程序員對此認識不夠,就容易埋下隱患。下面的例子可以很好的解釋前綴和后綴的區別。

int a=8,b=2,y;
y=a+++--b;

代碼執行后,y的值是多少?

這個例子并非是挖空心思設計出來專門讓你絞盡腦汁的C難題(如果你覺得自己對C細節掌握很有信心,做一些C難題檢驗一下是個不錯的選擇。那么,《The C Puzzle Book》這本書一定不要錯過),你甚至可以將這個難懂的語句作為不友好代碼的例子。但是它也可以讓你更好的理解C語言。根據運算符優先級以及編譯器識別字符的貪心法原則,第二句代碼可以寫成更明確的形式:

y=(a++)+(--b);

當賦值給變量y時,a的值為8,b的值為1,所以變量y的值為9;賦值完成后,變量a自加,a的值變為9,千萬不要以為y的值為10。這條賦值語句相當于下面的兩條語句:

y=a+(--b);
a=a+1;

1.8邏輯與’&&’和邏輯或’||’的陷阱

為了提高系統效率,邏輯與和邏輯或操作的規定如下:**如果對第一個操作數求值后就可以推斷出最終結果,第二個操作數就不會進行求值!**比如下面代碼:

if((i>=0)&&(i++ <=max))
 {
//其它代碼  
 }

在這個代碼中,只有當i>=0時,i++才會被執行。這樣,i是否自增是不夠明確的,這可能會埋下隱患。邏輯或與之類似。

1.9結構體的填充

結構體可能產生填充,因為對大多數處理器而言,訪問按字或者半字對齊的數據速度更快,當定義結構體時,編譯器為了性能優化,可能會將它們按照半字或字對齊,這樣會帶來填充問題。比如以下兩個個結構體:

第一個結構體:

struct {
char  c;
short s;
int   x;
 }str_test1;

第二個結構體:

struct {
char  c;
int   x;
short s;
 }str_test2;

這兩個結構體元素都是相同的變量,只是元素換了下位置,那么這兩個結構體變量占用的內存大小相同嗎?

其實這兩個結構體變量占用的內存是不同的,對于Keil MDK編譯器,默認情況下第一個結構體變量占用8個字節,第二個結構體占用12個字節,差別很大。第一個結構體變量在內存中的存儲格式如下圖所示:

0364a6f0-5017-11ed-a3b6-dac502259ad0.png

第二個結構體變量在內存中的存儲格式如下圖所示。對比兩個圖可以看出MDK編譯器是是怎么將數據對齊的,這其中的填充內容是之前內存中的數據,是隨機的,所以不能在結構之間逐字節比較;另外,合理的排布結構體內的元素位置,可以最大限度減少填充,節省RAM。

0375dd12-5017-11ed-a3b6-dac502259ad0.png

2 不可輕視的優先級

C語言有32個關鍵字,卻有34個運算符。要記住所有運算符的優先級是困難的。稍不注意,你的代碼邏輯和實際執行就會有很大出入。

比如下面將BCD碼轉換為十六進制數的代碼:

 result=(uTimeValue>>4)*10+uTimeValue&0x0F;

這里uTimeValue存放的BCD碼,想要轉換成16進制數據,實際運行發現,如果uTimeValue的值為0x23,按照我設定的邏輯,result的值應該是0x17,但運算結果卻是0x07。經過種種排查后,才發現’+’的優先級是大于’&’的,相當于(uTimeValue>>4)*10+uTimeValue與0x0F位與,結果自然與邏輯不符。符合邏輯的代碼應該是:

 result=(uTimeValue>>4)*10+(uTimeValue&0x0F);

不合理的#define會加重優先級問題,讓問題變得更加隱蔽。

038a7664-5017-11ed-a3b6-dac502259ad0.png

編譯器在編譯后將宏帶入,原代碼語句變為:

if(IO0PIN&(1<<11) ==(1<<11))
 {
//其它代碼   
 }

運算符'=='的優先級是大于'&'的,代碼IO0PIN&(1<<11) ==(1<<11))等效為IO0PIN&0x00000001:判斷端口P0.0是否為高電平,這與原意相差甚遠。因此,使用宏定義的時候,最好將被定義的內容用括號括起來。

按照常規方式使用時,可能引起誤會的運算符還有很多,如下表所示。C語言的運算符當然不會只止步于數目繁多!

039bc810-5017-11ed-a3b6-dac502259ad0.png有一個簡便方法可以避免優先級問題:不清楚的優先級就加上”()”,但這樣至少有會帶來兩個問題:

  • 過多的括號影響代碼的可讀性,包括自己和以后的維護人員
  • 別人的代碼不一定用括號來解決優先級問題,但你總要讀別人的代碼

無論如何,在嵌入式編程方面,該掌握的基礎知識,偷巧不得。建議花一些時間,將優先級順序以及容易出錯的優先級運算符理清幾遍。

隱式轉換

C語言的設計理念一直被人吐槽,因為它認為C程序員完全清楚自己在做什么,其中一個證據就是隱式轉換。C語言規定,**不同類型的數據(比如char和int型數據)需要轉換成同一類型后,才可進行計算。

**如果你混合使用類型,比如用char類型數據和int類型數據做減法,C使用一個規則集合來自動(隱式的)完成類型轉換。這可能很方便,但也很危險。

這就要求我們理解這個轉換規則并且能應用到程序中去!

  1. 當出現在表達式里時,有符號和無符號的char和short類型都將自動被轉換為int類型,在需要的情況下,將自動被轉換為unsigned int(在short和int具有相同大小時)。這稱為類型提升。

提升在算數運算中通常不會有什么大的壞處,但如果位運算符 ~ 和 << 應用在基本類型為unsigned char或unsigned short 的操作數,結果應該立即強制轉換為unsigned char或者unsigned short類型(取決于操作時使用的類型)。

uint8_t  port =0x5aU;
uint8_t  result_8;
result_8= (~port) >> 4;

假如我們不了解表達式里的類型提升,認為在運算過程中變量port一直是unsigned char類型的。我們來看一下運算過程:~port結果為0xa5,0xa5>>4結果為0x0a,這是我們期望的值。

但實際上,result_8的結果卻是0xfa!在ARM結構下,int類型為32位。變量port在運算前被提升為int類型:~port結果為0xffffffa5,0xa5>>4結果為0x0ffffffa,賦值給變量result_8,發生類型截斷(這也是隱式的!),result_8=0xfa。經過這么詭異的隱式轉換,結果跟我們期望的值,已經大相徑庭!正確的表達式語句應該為:

result_8=(unsigned char) (~port) >> 4;             /*強制轉換*/
  1. 在包含兩種數據類型的任何運算里,兩個值都會被轉換成兩種類型里較高的級別。類型級別從高到低的順序是long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。

這種類型提升通常都是件好事,但往往有很多程序員不能真正理解這句話,比如下面的例子(int類型表示16位)。

uint16_t  u16a = 40000;             /* 16位無符號變量*/
uint16_t  u16b= 30000;           /*16位無符號變量*/
uint32_t  u32x;                   /*32位無符號變量 */
uint32_t  u32y;
 u32x = u16a +u16b;                 /* u32x = 70000還是4464 ? */
 u32y =(uint32_t)(u16a + u16b);    /* u32y = 70000 還是4464 ? */

u32x和u32y的結果都是4464(70000%65536)!不要認為表達式中有一個高類別uint32_t類型變量,編譯器都會幫你把所有其他低類別都提升到uint32_t類型。正確的書寫方式:

u32x = (uint32_t)u16a +(uint32_t)u16b;      
//或者:
 u32x = (uint32_t)u16a + u16b;

后一種寫法在本表達式中是正確的,但是在其它表達式中不一定正確,比如:

uint16_t u16a,u16b,u16c;
uint32_t  u32x;
u32x= u16a + u16b + (uint32_t)u16c;/*錯誤寫法,u16a+ u16b仍可能溢出 */
  1. 在賦值語句里,計算的最后結果被轉換成將要被賦予值的那個變量的類型。這一過程可能導致類型提升也可能導致類型降級。降級可能會導致問題。比如將運算結果為321的值賦值給8位char類型變量。程序必須對運算時的數據溢出做合理的處理。很多其他語言,像Pascal(C語言設計者之一曾撰文狠狠批評過Pascal語言),都不允許混合使用類型,但C語言不會限制你的自由,即便這經常引起Bug。

  2. 當作為函數的參數被傳遞時,char和short會被轉換為int,float會被轉換為double。

當不得已混合使用類型時,一個比較好的習慣是使用類型強制轉換。強制類型轉換可以避免編譯器隱式轉換帶來的錯誤,同時也向以后的維護人員傳遞一些有用信息。這有個前提:你要對強制類型轉換有足夠的了解!下面總結一些規則:

  • 并非所有強制類型轉換都是由風險的,把一個整數值轉換為一種具有相同符號的更寬類型時,是絕對安全的。
  • 精度高的類型強制轉換為精度低的類型時,通過丟棄適當數量的最高有效位來獲取結果,也就是說會發生數據截斷,并且可能改變數據的符號位。
  • 精度低的類型強制轉換為精度高的類型時,如果兩種類型具有相同的符號,那么沒什么問題;需要注意的是負的有符號精度低類型強制轉換為無符號精度高類型時,會不直觀的執行符號擴展,例如:
unsigned int bob;
signed char fred = -1;
bob=(unsigned int )fred;    /*發生符號擴展,此時bob為0xFFFFFFFF*/


審核編輯 :李倩


聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • C語言
    +關注

    關注

    180

    文章

    7608

    瀏覽量

    137129
  • 嵌入式開發
    +關注

    關注

    18

    文章

    1033

    瀏覽量

    47609
  • 編譯器
    +關注

    關注

    1

    文章

    1636

    瀏覽量

    49173

原文標題:嵌入式開發中的C語言特性

文章出處:【微信號:c-stm32,微信公眾號:STM32嵌入式開發】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    新手怎么學嵌入式?

    的基礎上增加了面向對象編程的特性,有助于編寫更復雜的嵌入式程序。 3. 學習硬件知識 嵌入式技術與硬件緊密相關,因此你需要了解一些基本的硬件知識。比如微控制器(MCU)、微處理器(
    發表于 12-12 10:51

    嵌入式學習建議

    動手做一個實際的小系統,底層硬件基礎就有了。各個硬件模塊驅動程序編寫嵌入式系統的必備基礎。學習嵌入式系統的初期,這個過程是必須的。 ④至于嵌入
    發表于 10-22 11:41

    嵌入式主板是什么意思?嵌入式主板全面解析

    嵌入式主板,通常被稱為嵌入式系統的核心組件,是一種用于控制和數據處理的計算機硬件,其設計旨在嵌入特定設備中執行專門任務。嵌入式主板如同是設備的“大腦”,主要功能是根據需要管理和控制設備
    的頭像 發表于 09-30 10:05 ?599次閱讀

    七大嵌入式GUI盤點

    采用純C語言開發。它的作者是來自匈牙利的Gabor Kiss-Vamosikisvegabor,LVGL用C語言編寫,以實現最大的兼容性(與C++兼容),模擬器可在沒有
    發表于 09-02 10:58

    C28x嵌入式應用程序二進制接口

    電子發燒友網站提供《C28x嵌入式應用程序二進制接口.pdf》資料免費下載
    發表于 08-31 09:39 ?0次下載
    <b class='flag-5'>C</b>28x<b class='flag-5'>嵌入式</b>應用<b class='flag-5'>程序</b>二進制接口

    C6000嵌入式應用程序二進制接口

    電子發燒友網站提供《C6000嵌入式應用程序二進制接口.pdf》資料免費下載
    發表于 08-29 14:52 ?0次下載
    <b class='flag-5'>C</b>6000<b class='flag-5'>嵌入式</b>應用<b class='flag-5'>程序</b>二進制接口

    如何提升嵌入式編程能力?

    :掌握嵌入式系統的基本原理,包括中斷、并發、實時操作、低功耗設計等。 3. 實踐編程:通過實際編寫和測試代碼來提高技能。從簡單的LED閃爍程序開始,逐步過渡到更復雜的項目,如定時器PWM應用、串口、IIC
    發表于 06-21 10:01

    從事嵌入式方向,一定要軟硬件通吃?

    軟件工程師的職責嵌入式軟件工程師的主要職責是為嵌入式系統編寫和調試代碼,確保系統按預期工作。他們需要處理底層硬件接口、實時操作系統(RTOS)、驅動程序和應用層軟件。
    的頭像 發表于 06-05 08:10 ?1231次閱讀
    從事<b class='flag-5'>嵌入式</b>方向,一定要軟硬件通吃?

    再談嵌入式實時操作系統

    由于嵌入式處理器早期功能單一且運算能力不高,嵌入式應用已不能滿足各個領域不斷增長的需求。嵌入式操作系統應運而生,嵌入式操作系統可以支持新時代復雜、多任務環境和功能的
    的頭像 發表于 04-09 17:27 ?814次閱讀
    再談<b class='flag-5'>嵌入式</b>實時操作系統

    如何成為一名嵌入式C語言高手?

    相關教材、參加在線課程或者參考編程書籍來系統地學習C語言的基礎知識,并通過編寫簡單的程序進行實踐。 二、深入了解嵌入式系統的硬件架構和工作原理嵌入式
    發表于 04-07 16:03

    如何成為一名嵌入式C語言高手?

    相關教材、參加在線課程或者參考編程書籍來系統地學習C語言的基礎知識,并通過編寫簡單的程序進行實踐。 二、深入了解嵌入式系統的硬件架構和工作原理嵌入式
    發表于 03-25 14:12

    嵌入式工程師需要掌握哪些技術?

    一些必要的技術能力是至關重要的。在本篇中,我們將討論入行嵌入式所必須的技術能力。 1.C/C++編程能力:C/C++是
    發表于 03-04 16:38

    嵌入式學習步驟

    開發。 嵌入式學習步驟總結如下: (1).確定目標平臺:選擇適合您要開發的嵌入式系統的硬件平臺。這取決于您要控制的設備以及您需要執行的任務。 (2).選擇編程語言:嵌入式系統通常使用C
    發表于 02-02 15:24

    聊一聊嵌入式C語言

    作為一名嵌入式軟件開發者,熟練掌握嵌入式C語言對我的日常工作至關重要。
    的頭像 發表于 01-22 09:28 ?558次閱讀

    嵌入式自學好書推薦

    令、編輯器VI、編譯器GCC、調試器GDB以及Make項目管理工具、Shell和Makefile腳本編寫等知識,并搭建嵌入式開發環境。推薦書籍:《Linux從入門到精通》。 無論從就業市場還是技術發展的角度來看,嵌入式都是一個
    發表于 01-11 15:13
    主站蜘蛛池模板: 欧美成a人片免费看久久| 亚洲精品午夜久久久伊人| 91欧洲在线视精品在亚洲| 欧美日韩一级黄色片| 哒哒哒高清视频在线观看| 亚洲 欧美无码原创区| 久青草国产在线视频| 成人小视频在线观看免费| 性吧 校园春色| 老司机亚洲精品影院在线观看| 99re.05久久热最新地址| 色偷偷777| 久久青草费线频观看国产| 成年人在线免费观看视频网站| 亚洲精品国产A久久久久久| 又粗又大又爽又黄的免费视频| 久久影院午夜理论片无码| 亚洲专区中文字幕视频专区| 美女内射少妇三区五区| 超碰v| 亚洲视频一区| 欧洲最大无人区免费高清完整版 | 免费成人小视频| 国产精品VIDEOS麻豆TUBE| 3d在线看小舞被躁视频| 小小水蜜桃视频高清在线观看免费| 麻豆精品传媒2021网站入口| 国产精品女主播主要上线| 97午夜伦伦电影理论片| 亚洲 欧美 综合 高清 在线| 拍床戏被肉高H纯肉H在水| 久久机热免费视频| 国产精品自在自线亚洲| 9久久免费国产精品特黄| 亚洲男女羞羞无遮挡久久丫| 日韩视频中文在线一区| 美女PK精子小游戏| 寂寞夜晚视频在线观看| 国产99精品视频| qvod电影在线观看| 最近的2019中文字幕国语HD|