C筆記-左值與右值


前言:工欲善其事,必先利其器

兩種資料

學習編程語言, 有兩類資料可以讓人"高潮".

​ 一類是針對初學者而設計的入門類書籍, 這種書總是適時地結合生動的生活實例, 來讓啥都不懂的萌新理解一些基本的和關鍵的東西, 達到撥雲見日的效果. 為將來的進一步學習培養出良好的興趣和打下堅實的基礎. 最具代表性的就是 headfirst 系列叢書.

​ 而另一類資料, 便是標准文獻了. 它就像博學的導師或者修仙小說里的隨身老爺爺, 能夠完美地解答你的任何疑惑(就算有解答不了的問題, 那也是暫時的, 因為標准文獻本身也是不斷改進和迭代的).

​ 這邊作者假設讀者都有一定的C基礎,不是啥都不懂的萌新, 但是對於左值和右值的概念仍存有疑惑的朋友, 另外作者水平有限, 如有錯誤和瑕疵, 歡迎各位朋友指正.


參考資料及其使用說明

參考資料

​ 本文的參考資料是C11標准文獻草案(N1570), 是免費且幾乎等同於C11標准文獻的版本.

  • 外網版C11標准文獻資料(需FQ)

    html版

    pdf版

  • 筆者提供的國內版(筆者自建站)

    html版

  • 筆者所提供的本地下載(7z壓縮包, 內含pdf與html版)

    本地下載

本文的鏈接及資料使用說明

  • 本文鏈接說明

    本文的鏈接部分,均是國內html版的鏈接

  • 本地下載的資料說明

    • c11標准文獻不僅每一個章節都有編號, 且每一個自然段都有編號,方便定位

    • c11標准的html版: 可以用錨點直接定位到對應章節, 自然段 以及 注解

      • 錨點: 形如 #6.3.1.2p3 的東西, 出現在網址欄的最后, 用於定位到網頁中的位置(滾輪會自動滾到對應內容處)

      • c11標准html版的錨點構成說明:

        示例1: #6.3.1.2p3

        • 6.3.1.2是具體的章節編號: 第6章第3部分1小節第2節
        • p3是對應的自然段編號: p3代表第3自然段

        示例2: #note99

        • note99是對應的注解編號: note99代表第99個注解
      • 應用說明:

        • 查看國內版c11標准的第3章第2部分7小節第4自然段,可以直接輸入以下網址: peterzhang.cool:3000/pdfs/c11.html#3.2.7p4,然后回車
        • 查看本地下載的c11的html版本也可以打開c11.html之后,在網址后面加上#3.2.7p4,然后按回車即可

官方對於左值和右值的定義

​ 可見, 左值右值的概念來自賦值表達式, =號左邊的為左值(可修改的左值), 它代表(定位)了一個可用於存放數據的存儲空間; 而右值通常被理解為 "表達式的值"(value of an expression).

實際使用時的疑問

​ 那么到底哪些是左值, 哪些又屬於右值? 什么情況下屬於左值, 什么情況下屬於右值呢?


左值的涵蓋范圍

  • 變量名

  • 指針變量

  • 一些運算符的運算結果:

    • * -- 取內容運算符
    • [] -- 數組下標運算符
    • (type-name){initialize-list} -- 復合字面量
    • . (只有左操作數為左值時,結果才為左值)
    • ->(無論左操作數為左值還是右值,結果均為左值)

    舉例說明:

    • a是數組名,絕大部分情況下屬於指針值(見后續部分),是右值
    • a[1]屬於運算符[]的結果, 屬於左值, 可以放在等號左邊進行賦值操作.

重要概念: 左值轉化(lvalue conversion)

#6.3.2.1p2: 滿足以下條件的左值會被轉化成對應的存儲空間(數據對象)中所存儲的值,並且不再是一個左值, 這一過程被稱為 左值轉化

  • 不是 sizeof, _Alignof, &, ++, -- 運算符的操作數

  • 不是 . 或 賦值運算符的左操作數

  • 該左值不是數組類型(數組類型的左值按其他規定進行轉化)

    • 一維數組: 不是數組名,但可以是數組元素

    • 多維數組: 不是任意N維(N>1)的數組名或數組元素,但可以是一維的數組元素

      (也就是說: 二維數組arr[][]中, arr[1]仍舊代表一個數組, 等同於一個數組名,不滿足左值不是數組類型的條件)


左值與指針

概念上的區別

  • 左值: 可以放在賦值號的左邊, 與一個存儲單元(數據對象)對應, 代表了可直接獲取和設置該單元內容的途徑. (左值就像是一個已經撥通且未掛斷的電話)
  • 指針值: 某一數據的存儲位置的信息. (指針值就像是一個電話號碼)

通過左值, 你可以通過它直接獲取和設置存儲單元(數據對象)中的內容, 就像你可以直接問已撥通電話的另一頭問題或告訴另一頭一些信息; 而指針值, 就像一個電話號碼, 想要像左值那樣獲取或設置內容, 必須先要 "按照號碼撥打電話", 這一步驟通常由取內容運算符 * 完成. 如果我們用另一個變量保存這個 "電話號碼", 這個變量就成了 "指針變量".

注意: 指針變量是一個變量, 它是左值, 而指針值並不是左值.

舉例: (我們把其他人當作是一個存儲空間,而你扮演主程序)

你正在跟小張通電話 -- 左值 <==> int a;

你手里有小張的電話號碼 -- 指針值 <==> &a;

你通過給小劉打電話,獲取了小張的電話號碼,然后再給小張打電話告訴他一些事 -- 利用指針變量 <==> int *p = &a; *(p) = 314;

左值與指針值的互相轉化

我們聲明的變量名是一類天然的左值, 它就像是我們和朋友直接面對面說話(或者一通已打通的電話); 而有時候,我們需要交談的對象並不在我們身邊, 這時候就需要我們自己去撥打電話.

  • 將指針值轉化為對應的左值: 取內容運算符*
  • 獲取某一左值的指針值: 取地址運算符&

指針值的構成

補充知識:存儲單元的地址編排

  • 地址編號是基於字節的: 一個字節對應一個地址編號, 地址值(指針值)只能指向單個字節

  • 除了char外,C中的數據類型是多字節

  • 讀取多字節數據的策略:

    • 地址值(指針值)指向存儲單元的第一個字節

    • 定義一個取值范圍, 說明取得數據的長度

指針值的構成

  • 指針值/地址值: 指向存儲空間的起始字節
    • 指針值的存儲類型是無符號的多字節數值
    • 指針指向的類型(int *p;中的int)並不影響指針值的sizeof大小
  • 指針指向的類型: 規定 利用指針進行一次內容讀取/內容設置 所影響的字節范圍
    • 一次讀取或設置: 同時操作包含起始字節在內的N個字節(N由指針指向的類型確定)
    • 指針變量增加或減少1: 地址值/指針值增加或減少N

圖示:

測試代碼: test.c

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    short int test = 314;
    int *pInt = &test;
    float *pFloat;
    double *pDouble;
    long double *pLongDouble;

    printf("The sizeof short int is %d\n",sizeof(short int)); //2

    //指針(地址)是一個獨立的數據存儲類型,類似於int,double等,占用的內存大小相同
    printf("The sizeof pInt is %d\n",sizeof(pInt)); //4
    printf("The sizeof pFloat is %d\n",sizeof(pFloat)); //4
    printf("The sizeof pDouble is %d\n",sizeof(pDouble)); //4
    printf("The sizeof pLongDouble is %d\n",sizeof(pLongDouble)); //4

    //指針指向的類型確定讀取的字節范圍
    printf("The address of test is %p\n",pInt);
    printf("Input the address above and use it without a type bounded:\n");
    unsigned long long p;
    scanf("%x",&p); //手動輸入上面打印的地址值
    printf("The value of p is %lx\n",p);
    printf("The value of *(short int *)p is %d\n",*(short int *)p); //314(10)
    printf("The value of *(char *)p is %d\n",*(char *)p); //只讀取后8位,所以是58(10)

    //指針變量+1,指針值/地址值的變化?
    short int *pTest = &test;
    printf("The address of test is %p\n",pTest);
    pTest++;
    printf("The address of test now is %p\n",pTest);

    getchar();
    return 0;
}

控制台輸出:

數組名與數組下標運算

#6.3.2.1p3: 滿足下列條件的數組類型值(通常是數組名)會被轉換為一個指向該數組首個元素的首個字節的指針值(注意,不是指針變量而是指針值):

  • 數組名不是sizeof或&的操作數
  • 不是用來初始化一個數組的數組字面量

因此:

  • 數組名本身是屬於左值的, 但是這並沒有什么卵用

  • 因為絕大多數情況下(包括位於賦值號左邊的時候),數組名會被轉換為指針值(不再是左值)

  • 數組名經過下標[]運算或*運算符卻會變成左值,代表數組內某一元素,可以用於賦值


運算符歸納表格及實例說明

各種運算符運算結果左右值類型總結表

實例分析

  • 復合字面量(compound literial)

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int p = ((int){314})++; //works just fine
        printf("p is %d\n",p); //314
    
    	//int *p = ((int [2]){314,110})++; //error: lvalue required as increment operand
    
        getchar();
        return 0;
    }
    

    分析:

    • int p = ((int){314})++;

      復合字面量(int){314}生成一個未命名的左值(其值為314)

      對該左值應用后綴形式的++運算符,生成一個右值(314)

      將該右值賦值給變量p

    • int *p = ((int [2]){314,110})++; //報錯語句

      復合字面量(int [2]){314,110}生成一個未命名的數組左值

      數組左值經過轉化,變成指向該數組第一個元素的指針值(右值)

      對該指針值應用后綴++運算符報錯(++運算符的操作數必須是左值)

  • 結構體相關運算符(*與->)

    結構體運算符 . :

    #include <stdio.h>
    #include <stdlib.h>
    
    //聲明結構體s
    struct s { double i; };
    
    //聲明聯合體g
    union {
        struct {
            int f1;
            struct s f2;
        } u1;
        struct {
            struct s f3;
            int f4;
        } u2;
    } g;
    
    struct s f(void){ //返回結構體s的函數
        return g.u1.f2; //返回g.u1.f2
    }
    
    int main(void)
    {
        //測試: 結構體變量
        struct s varible = {3.1415};
        varible.i++;
        printf("varible.i.i is %f\n",varible.i); //4.1415
    
        //測試: 結構體返回值函數
        struct s f(void);
        //f().i = 20.0; //error: lvalue required as left operand of assignment
    
        getchar();
        return 0;
    }
    

    分析:

    • varible.i++;語句工作正常: 說明其執行結果為左值
    • f().i = 20.0;語句報錯: 說明f().i不是左值
      • 函數調用的返回值是右值(盡管它返回的是文件域的聯合體變量的成員的內容)
      • 右值.i,根據C11標准的規定,其執行結果也是右值,因此報錯

    結構體指針運算符->:

    #include <stdio.h>
    #include <stdlib.h>
    
    struct s { double i; };
    union {
        struct {
            int f1;
            struct s f2;
        } u1;
        struct {
            struct s f3;
            int f4;
        } u2;
    } g;
    
    struct s * f(void){ //返回結構體指針的函數
        return &(g.u1.f2);
    }
    
    int main(void)
    {
        //測試: 結構體指針返回值函數
        struct s * f(void);
        f()->i = 20.0;//結構體指針指向的成員是左值
        printf("return value is: %f\n", f()->i);
        struct s newS = {3.14};
        *(f()) = newS; //函數返回的結構體指針也是右值,用*之后才變為左值
        printf("Now,value is: %f\n", f()->i);
    
        getchar();
        return 0;
    }
    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM