講解運算子

BruceeChou發表於2019-03-19

一、概念

1.1 含義

運算子用於執行程式程式碼運算,會針對一個及以上運算元來進行運算。

1.2 特點

  • 優先順序和結合性:先考慮優先順序,再考慮結合性。同一優先順序的運算子結合性相同(用於消除歧義)。

  • 一般而言:單目 > 雙目 > 三目(例外 ?: > 賦值運算子);算術 > 關係 > 邏輯 > 賦值。當我們提到幾目運算子,實際上談論的是運算元的個數。

  • 優先順序被劃分為 15 個等級。

1.3 典型例題

優先順序1 初等運算子

例一:若有一下說明和語句:能正確引用陣列arr元素的選項是( D )

int arr[4][5];    //二維陣列。
int (*ptr)[5];    //指向陣列的指標。
ptr = arr;
  • A ptr+1    //代表 &arr[1]    

  • B *(ptr+3)   //代表 arr[3] 陣列名也是一個地址。

  • C *(ptr+1)+3   //代表 &arr[1][3] 

  • D *(ptr[0]+2)    //代表 *(&arr[0][2])

在C語言中,連續記憶體的表示形式是“起始地址+偏移量”。現有一陣列int arr[5] = {1, 2, 3, 4, 5};對一般的內建型別或者指標型別進行 & 是取得變數的首地址,但是如果對集合型別 陣列名取地址呢?&arr  型別是 int(*)[5],是不能直接賦值給 int **的。表面理解——型別不同,主要體現在+1的能力上。先展示我在VS2017下的執行結果吧:

008F F6E0 //arr首地址,列印輸出作為基準。
008F F6F4 //選項A
008F F71C //選項B
008F F700 //選項C
1973      //選項D 我把陣列元素全部填充成1973。
for(int i = 0; i < 20; ++i)
    arr[i / 5][i % 5] = 1973;

1)記憶體就是記憶體,取決於編譯器如何看待;2)型別型別還是型別!地址是冰冷的一串數字,需要我們去揣摩如何解釋它。3)指標解引用之後就是取得所指之物的內容,多少級指標都是這樣的。    

選項A ptr是指向陣列的指標,解引用之後獲得整個陣列!ptr+1的值會和陣列首地址相差20byte,14H,正好是20byte。

選項B ptr+3指向的是arr[3],解引用之後就獲得了arr[3],會和陣列首地址相差60byte,3CH,正好是60byte。

選項C ptr+1指向arr[1],解引用之後獲得了arr[1],陣列名+3,實際上得到的是&arr[1][3],會和陣列首地址相差32byte,
20H,正好是32byte。

選項D 出現了 [] 運算子,它等同於 *(基地址 + 偏移量)。ptr[0]效果等同於arr[0],ptr[0]+2等同於&arr[0][2],再進行解引用就取出了元素值。 ------出自(《組合語言第3版王爽》P23 )基地址+偏離量是一對很重要的概念。8086CPU採用段基址×16+偏移地址等於實體地址的手段,而且這種處理方式一直被延續下來。

例二 如下哪一段程式碼不能給地址 0xaae0275c 賦值為 1 ? (   )

A volatile int *p = (int *)0xaae0275c; *p = 1;
B (volatile int *)0xaae0275c[0] = 1;
C volatile int *p = (int *)0xaae0275c; p[0] = 1;
D *(volatile int *)0xaae0275c = 1;

B 選項, [ ] 優先順序最高,結合之後就會導致問題。應修改成 ((volatile int *)0xaae0275c)[0] = 1;
例三

struct str_t
{
	long long m_len;
	char m_data[32];    //字元陣列型別。
};
struct data1_t
{
	long long m_len;
	int m_data[2]; //整型陣列型別。
};
struct data2_t
{
	long long m_len;
	char *m_data[1];    //指標陣列型別。
};
struct data3_t
{
	long long m_len;
	void *m_data[];     //二級指標。 實際上會取用4位元組。
};

int main()
{
	struct str_t str;
	memset(&str, sizeof(struct str_t);

	str.m_len = sizeof(struct str_t) - sizeof(long long);
	sprtinf(str.m_data, "%s", "BruceeLee");
	_____________________________________;
	return 0;
}

問:下列程式碼不能正確輸出 BurceeLee(世界著名武術家)選項是( B )

  • A struct data3_t *pData = (struct data3_t*)&str; printf("%s\n", (char*)(&(pData->m_data[0]));    

  • B struct data2_t *pData = (struct data2_t*)&str; printf("%s\n", (char*)(pData->m_data[0]));

  • C struct data1_t *pData = (struct data1_t*)&str; printf("%s\n", (char*)(pData->m_data));

  • D struct str_t *pData = (struct str_t*)&str; printf("%s\n", (char*)(pData->m_data));

解答:明確思路,只要我能拿到"BruceeLee"的首地址,不管是什麼型別的地址,進行強轉之後就能正確輸出。->和[]優先順序相同,結合性是從左至右。 四個結構體成員,只是m_data型別不同。

選項A m_data是指標陣列,pData->m_data實際上就是獲得指標陣列,緊接著pData->m_data[0]就訪問資料首元素,對陣列首元素取地址,地址是和字串"BruceeLee"起始地址重合的,進行強轉之後就能正確輸出。    

選項B m_data是指標陣列,pData->m_data獲得該陣列,pData->m_data[0]就訪問到陣列首元素,實際上就是將'B' 'r' 'u' 'c'四個位元組的資料拼接成一個char*地址,從而形成野指標

選項C m_data是一個整型陣列,pData->m_data實際上就是獲得了該陣列,將陣列名強轉成char*,實際上和字串起始地址是重合的,也能正確列印輸出。

選項D m_data是一個字元陣列,原理和選項C相同。

例四 

宣告一個指向含有10個陣列元素的指標,其中每個元素是一個函式指標,該函式的返回值為int,引數是int*。下面選項正確的是( )

  • A(int *p[10])(int*)

  • B int [10]*p(int *)

  • C int (*(*p)[10])(int *)

  • D int ((int *)[10])*p

  • E 以上選項都不正確

宣告一個變數從識別符號入手,題中要求是指標型別的變數,則有(*)p;而後該變數指向一個陣列,則有(*p)[10];而後需要描述陣列元素型別——指標型別,則有(*(*p)[10]);最後需要描述具體是什麼型別的指標,int(*(*p)[10])(int*)。

優先順序2   ++   ~    *

 例一

int i = 4;
int j = i++ +1; //執行貪心策略,吞下儘可能多的+。

解答:j = 5;因為後置自增是單目運算子,+是雙目運算子。先返回4再加1得到5然後i自增成5。

 例二

*p++的效果是?

自增運算子和指標運算子同優先順序,右結合性(優先順序相同結合性一定得相同),等同於*(p++)。

 例三

執行下列語句後的結果為()

int x = 3, y;
int *px = &x;
y = *px++;

優先順序相同,才考慮結合性,右結合性,等同於 y = *(px++)。先將解引用的值3賦給y,然後p前進一步(此時是沒有問題的),除非嘗試通過這個指標訪問指向的地址空間。如果題目改為 y = *++px;結果就是不可預知的。

 例四

短路求值。

int main()
{
    int i = 1, j = 1, k = 2;
    if( (j++ || k++) && i++)
        cout << i << " " << j << " " << k << endl;
    return 0;
}

能想到這一點,就知道結果是 2 2 2 。 

 例五

下面程式的執行結果是( )

int main()
{
    int a = 1, b = 10;
    do{
        b -= a;
        a++;
    }while(b-- < 0)
    {
        printf("a = %d, b = %d\n", a , b);
    }
    return 0;
}

 do..while,do語句一定會執行1次。所以結果為 a = 2, b = 8。

 例六

int x = 1;
int y = ~x;

問 y 的值是多少?

由補碼的性質可知:y = -2。 

 例七

int main()
{
    char str[] = "ABCD";
    char *p = str;
    printf("%d\n", *(p+4));
    return 0;
}

p指向陣列起始位置,p+4指向陣列結束標誌'\0',%d列印輸出就是0。此處容易犯的一個問題就是會誤以為本程式有編譯錯誤。 

例八

下列程式的輸出結果是( )

int main()
{
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, *p = a + 3;
    printf("%d\n", *++p);
    return 0; 
}
  • A 3

  • B 4

  • C a[4]的地址

  • D 非法

解答:“基地址+偏移量”模式似乎為C語言學習者所熱愛。

例九 函式pa和函式pb哪個執行更快?

#define NUMA 10000000
#define NUMB 1000
int a[NUMA], b[NUMB]

void pa()
{
    int i, j;
    for(i = 0; i < NUMB; ++i)
    {
        for(j = 0; j < NUMA; ++j)
            ++a[j];
    }
}

void pb()
{
    int i, j;
    for(i = 0; i <NUMA; ++i)
    {
        for(j = 0; j < NUMB; ++j)
        {
            ++b[j];
        }
    }
}

這是阿里巴巴的一道筆試題,乍一看之下,很無厘頭啊,但是細想,就能感受到出題者的心思縝密——主要考察程式的“區域性性原理”。 因為區域性性原理,所以記憶體利用效率更高,同時帶來另一個問題——缺頁(命中率)。因為陣列a比陣列b大很多,所以可能跨越更多的頁,缺頁率更高或者命中率更低。所以 函式pb 比 函式pa 快。

優先順序3   * / %

例一 

若有定義: int a = 7; float x = 2.5, y = 4.7;則表示式 x + a % 3 * (int)(x + y) % 2 / 4的值是多少?

解答:乘、除、取餘三者優先順序相同,都是左結合性從左到右耐心計算可以得到值為2.5。

例二 

#include<iostream>
using namespace std;

int main()
{
    int a = 2;
    int b = ++a;
    cout<< a / 6 << endl;
    return 0;
}

解答: / 兩邊都是整型,所以運算之後的結果還是整型,故而輸出0。 

優先順序4

優先順序5  移位運算子

例一 以下程式碼執行後,val的值是___?

unsigned long val = 0;
char a = 0x48;
char b = 0x52;
val = b << 8 | a;

 表示式的值和變數的值,這是兩碼事。val = 0x5200 | 0x48 = 21064。

優先順序6

優先順序7

優先順序8    &

例一 

返回偶數。

int fun(int x)
{
    if(x % 2)    //x是奇數。
    {
        return x - 1;    
    }
    else
        return x;
} 

用表示式 x & -2;代替,以下說法不正確的是( C)

  • A 計算機的補碼錶示使得兩段程式碼等價

  • B 用第二段程式碼執行起來會更快一些

  • C 這段程式碼只適用於x為正數的情況

  • D 第一段程式碼更適合閱讀

不管輸入什麼,只需要把最低位抹成0,就實現了我們的目的,這就是位運算的魅力之所在。

優先順序9

優先順序10

優先順序11

優先順序12

優先順序13

優先順序14

優先順序15

二、擴充套件內容

2.1 運算子過載 

三、參考文獻

【1】友元函式過載

相關文章