C++ 學習筆記之(4)-表達式、運算符與類型轉換

C++ 學習筆記之(4)-表達式、運算符與類型轉換

表達式由一個或多個運算對象組成,對表達式求值將得到一個結果。字面值和變量是最簡單的表達式,其結果就是字面值和變量的值,把運算符和運算對象組合可以生成較複雜的表達式

基礎

基本概念

  • 對於含有多個運算符的複雜表達式來說,要想理解它的含義必須要理解運算符的優先級、結合律以及運算對象的求職順序

  • 表達式求值過程中,運算對象通常由一種類型轉換成另一張類型。

  • 左值(lvalue):對象的身份,即在內存的位置

    • 賦值運算符需要一個(非常量)左值作爲其左側運算對象,結果也是左值
    • 取地址符作用域一個左值運算對象,返回一個指向該運算對象的指針,這個指針是右值
    • 內置解引用運算符、下標運算符的求值結果都是左值
    • 內置類型和迭代器的遞增遞減運算符作用域左值運算對象
  • 右值(rvalue):對象的值(內容)

  • 關鍵字decltype作用於左值,得到的是引用類型

    int a = 0;
    int *p = &a;
    decltype(*p) b;  // 解引用運算符是生成左值,所以 b 結果是 int &, 即引用類型,未初始化
    decltype(&p) c;  // 取地址符生成右值,所以 c 結果是 int **, 指針的指針

求值順序

優先級規定了運算對象的組合方式,但是沒有說明運算對象按照什麼順序求值,如果表達式指向並修改了同一個對象,將會引發錯誤併產生未定義的行爲。

int i = f1() * f2();  // f1 和 f2 會在乘法之前調用,但卻不知道 f1 和 f2 的執行順序
int i = 0;
cout << i << " " << ++i << endl;  // 未定義,可能輸出1 1,也可能是0 1

算術運算符

這裏寫圖片描述
* 算數運算符能夠作用於任意算數類型,以及任意能轉換成算數類型的類型。算術運算符的運算對象和求值結果都是右值

  • 一元負號運算符對運算對象值取負後,返回其(提升後的)副本

    int i = 1024;
    int k = -1;  // k 是 -2014
    bool b = true;
    bool b2 = -b;  // b2 是 true
  • 布爾值不參與運算,如上代碼所示,bool類型的運算隊形先被提升爲int類型1,求負或爲-1,不爲0,故b2爲真

  • 取餘:若m % n 不等於0, 則它的負號和m相同。

邏輯和關係運算符

logistic_and_relation_operator

  • 對這兩類運算符來說,運算對象和求值結果都是左值
  • 短路求值:即當且僅當左側運算對象無法確定表達式的結果時纔會計算右側運算對象的值,對邏輯與和邏輯或操作符。
  • 進行比較運算時,除非比較的對象是布爾類型,否則不要使用布爾字面值truefalse作爲運算對象

賦值運算符

  • 賦值運算符的左側運算對象必須是一個可修改的左值
  • 對於複合運算符(+=, -= 等)都完全等價於a = a op b; , 唯一的區別是左側運算對象的求值次數:使用複合運算符只求值一次,使用普通的運算符則求值兩次(一次計算,一次賦值)

遞增和遞減運算符、成員訪問運算符、條件運算符

  • 前置版本(++a):將對象本身作爲左值返回
  • 後置版本(a++):將對象原始值的副本作爲右值返回
auto pbeg = v.begin();
*pbeg++;  // 正確, 返回*pbeg, 然後++pbeg; 因爲後置遞增運算符優先級高於解引用運算符

string s1 = "a string", *p = &s1;
*p.size();  // 錯誤: p是一個指針,沒有名爲size的成員, 因爲解引用運算符優先級低於點運算符

string finalgrade = (grade < 60) ? "fail" : "pass";

位運算符

bit_operator

  • 左移:右側插入0
  • 右移:依賴於其左側運算對象的類型
    • 無符號類型:左側插入0
    • 帶符號類型:左側插入符號位的副本或者值0, 如何選擇視具體環境而定

注意:位運算是一個很重要的知識點,需要深入學習

sizeof 運算符

sizeof運算符返回一條表達式或一個類型名字所佔的字節數,結果爲一個size_t類型的常量表達式

  • sizeof運算符的運算對象有兩種形式
    • sizeof (type)
    • sizeof expr: 返回表達式結果類型的大小。sizeof並不實際計算其運算對象的值
  • sizeof的運算對象中解引用一個無效指針仍然是一種安全的行爲,因爲指針實際上並沒有真正使用,sizeof不需要真的解引用指針也能知道它所指對象的類型。
  • sizeof運算符的結果部分地依賴於其作用的類型:
    • charchar類型表達式:1
    • 引用類型:被引用對象所佔空間的大小
    • 指針:指針本身所佔空間的大小
    • 解引用指針:指針指向的對象所佔空間的大小,指針不需要有效
    • 數組:整個數組所佔空間的大小,sizeof運算不會把數組轉換成指針來處理
    • string對象或者vector對象:該類型福鼎部分的大小,不計算對象中的元素佔用了多少空間

類型轉換

C++語言不會直接操作兩個不同類型的值,而是先根據類型轉換規則設法將運算對象的類型統一後再運算,由於是自動執行,故被稱作隱式轉換

何時發生隱式類型轉換

  • 在大多是表達式中,比int類型小的整形值首先提升爲較大的整數類型
  • 在條件中,非布爾值轉換成布爾類型
  • 初始化過程中,初始值轉換成變量的類型;賦值語句中,右側運算對象轉換成左側運算對象的類型
  • 如果算術運算或關係運算的運算對象有多種類型,需要轉換成同一種類型。
  • 函數調用也會發生類型轉換(後面學習)

算數轉換

算數轉換是把一種算數類型轉換成另一種算數類型

  • 運算符的運算對象轉換成最寬的類型,比如一個運算對象爲long double, 則另一個運算對象必會轉爲long double
  • 表達式既有浮點類型也有整數類型時,整數值將轉換成相應的浮點類型
  • 整形提升:小整數類型轉換成較大的整數類型
    • bool、char、signed char、unsigned char、short、unsigned short,如果int放得下,就提升爲int,否則提升爲unsigned int類型
    • 較大的char類型(wchar_t, char16_t, char32__t)提升爲int,unsigned int, long, `…..等中最小的類型,前提是放得下
  • 某運算對象爲無符號類型
    • 無符號 >= 帶符號類型:帶符號的運算對象轉換成無符號,如果帶符號的值爲負值,則結果無法判斷
    • 無符號 < 帶符號:轉換結果依賴於機器,
    • 如果無符號類型的所有值都能存在該帶符號類型中,則無符號運算對象轉換爲帶符號類型
    • 若不能,則帶符號類型的運算對象轉換成無符號類型

其他隱式類型轉換

  • 數組轉換成指針:在大多數用到數組的表達式中,數組自動轉換成指向數組首元素的指針
  • 指針的轉換
    • 常量整數值0或者字面值nullptr能轉換成任意指針類型
    • 指向任意非常量的指針能轉換成void *
    • 指向任意對象的指針能轉換成const void *

顯示轉換

命名的強制類型轉換cast-name<type>(expression);, 其中cast-namestatic_cast, dynamic_cast, const_castreinterpret_cast

static_cast

任何具有明確定義的類型轉換, 只要不包含底層const, 都可以使用static_cast

  • 當需要把一個較大的算數類型賦值給較小的類型時, static_cast非常有用,強制類型轉換表示:我知道並且不在乎精度損失,故編譯器不會出現警告信息

  • static_cast可用於編譯器無法自動執行的類型轉換

    void *p = &d;  // 正確:任何非常量對象的地址都能存入void *
    double *dp = static_cast<double*>(p);  // 正確:將void *轉換成初始的指針類型.如果類型不符,則未定義

const_cast

  • 只能改變運算對象的底層const,即可用來將常量對象轉換成非常量對象的行爲。

    一旦去掉了對象的const性質,編譯器就不再阻止我們對該對象進行寫操作了。若對象本身不是常量,使用強制類型轉換獲得寫權限是合法的行爲。但若對象是一個常量,在使用const_cast執行寫操作就會產生未定義的後果。

  • 其他形式的命名強制類型轉換改變表達式的常量屬性都將引發編譯器錯誤

  • const_cast常常用於有函數重載的上下文中

reinterpret_cast

通常爲運算對象的位模式提供較低層次上的重新解釋。reinterpret_cast本質上依賴於機器,非常危險,需要對涉及的類型和編譯器實現轉換的過程非常瞭解

int *ip;
char *pc = reinterpret_cast<char*>(ip);  // 等價於 char *pc = (char *) ip;
string str(pc);  // 運行時錯誤,pc 實質上指的是一個int

dynamic_cast

  • dynamic_cast的使用形式如下所示, type必須是一個類類型,並且通常含有虛函數

    • dynamic_cast<type*>(e):e必須是一個有效的指針
    • dynamic_cast<type&>(e):e必須是一個左值
    • dynamic_cast<type&&>(e): e不能使左值
  • e 的類型必須符合一下三個條件中的任意一個

    • 目標type的共有派生類
    • 目標type的共有基類
    • 目標type的類型
  • 轉換失敗時

    • 若目標是指針類型:結果爲0
    • 若目標是引用類型:拋出bad_cast異常
    // 指針類型的 dynamic_cast
    // bp 指針指向 Base(至少含有一個虛函數), Derived 是 Base 的共有派生類
    if(Derived *dp = dynamic_cast<Derived*>(bp))
    {
      // 轉換成功, 使用 dp 指向的 Derived 對象
    }else{  // bp 指向一個 Base 對象
      // 轉換失敗, 使用 dp 指向的 Base 對象
    }
    
    // 引用類型的dynamic_cast
    // 因爲不存在控引用,對於引用失敗,應該捕獲異常
    void f(const Base &b)
    {
      try{
          const Derived &d = dynamic_cast<const Derived&>(b);
            // 使用 b 引用的 Derived 對象
      }catch(bad_cast){
          // 處理類型轉換失敗的情況
      }
    }

運算符優先級表

operator_priority_table

結語

  • 對於多運算符的表達式,理解優先級、結合律和求值順序
  • 類型轉換非常重要,要專門深入學習
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章