C中一些小問題

原文鏈接:http://github.tiankonguse.com/blog/2014/12/05/c-base.html

轉自http://github.tiankonguse.com/blog/2014/12/05/c-base.html

1遍歷數組

問題:
有時候我們要遍歷一個不知道大小的數組,但是我們有數組的名字,於是我們可以通過 sizeof 獲得數組的大小了。
有了大小我們就可以遍歷這個數組了。
一般情況下大家都是從下標 0 開始計數,於是從來不會遇到下面的問題。
如果你遇到下面的問題你能想出是什麼原因嗎?

代碼:

#include<stdio.h>
#define TOTAL_ELEMENTS (sizeof(array) / sizeof(array[0]))
int array[] = {23,34,12,17,204,99,16};
int main() {
    for(int d=-1; d <= (TOTAL_ELEMENTS-2); d++) {
        printf("%d\n",array[d+1]);
    }
    return 0;
}

輸出是

--------------------------------
Process exited after 0.1068 seconds with return value 0
請按任意鍵繼續. . .

分析
sizeof 返回的類型是 unsigned int .
unsigned int 與 int 進行運算還是 unsigned int。
然後 -1 和 unsigned int 比較,會先把 -1 轉化爲 unsigned int。
這樣 -1 的 unsigned int 就很大了,所以沒有輸出了。

2. do while

問題;
大家在 do while 中使用過 continue 嗎?
沒有的話來看看這個問題吧。

代碼:

#include<stdio.h>
int main() {
    int i=1;
    do {
        printf("%d\n",i);
        i++;
        if(i < 15) {
            continue;
        }
    } while(false);
    return 0;
}

輸出:

1
--------------------------------
Process exited after 0.08437 seconds with return value 0
請按任意鍵繼續. . .

分析:
for 循環遇到 continue 會執行for 小括號內的第三個語句。
while 和 do…while 則會跳到循環判斷的地方。

3. 宏展開

問題:
大多數情況下,我們的宏定義常常是嵌套的。
這就涉及到展開宏定義的順序了。
下面來看看其中一個問題。

代碼:

#include <stdio.h>
#define f(a,b) a##b       //##是一個連接符號,用於把參數連在一起
#define g(a)   #a       //#是‘字符串化’的意思。把跟在後面的參數轉成一個字符串
#define h(a) g(a)
int main() {
    printf("%s\n",h(f(1,2)));
    printf("%s\n",g(f(1,2)));
    return 0;
}

輸出:

12
f(1,2)
--------------------------------
Process exited after 0.1038 seconds with return value 0
請按任意鍵繼續. . .

分析:
宏會掃描一遍,把可以展開的展開,展開一次後會再掃描一次看又沒有可以展開的宏。
兩個過程,第一個

  ↓ 
> h(f(1,2))
    ↓ 
> g(f(1,2))
    ↓
> g(12)
  ↓
> g(12)
  ↓
> "12"

第二個:

  ↓
> g(f(1,2))
  ↓
> "f(1,2)"

4 print返回值

問題:
你知道 printf 的返回值是什麼嗎?
猜猜下面的代碼輸出是什麼吧。

代碼:

#include <stdio.h>
int main() {
    int i=43;
    printf("%d\n",printf("%d",printf("%d",i)));
    return 0;
}

輸出:

4321

--------------------------------
Process exited after 0.112 seconds with return value 0
請按任意鍵繼續. . .

分析:
printf 的返回值是輸出的字符的長度。
所以第一個輸出 43 返回2.
第二個輸出 2 返回 1. 第三個輸出1. 於是輸出的就是 4321 了。

5數組參數

問題:
對於函數傳參爲數組時,你知道到底傳的是什麼嗎?

代碼:

#include<stdio.h>
#define SIZE 10
void size(int arr[SIZE][SIZE]) {
    printf("%d %d\n",sizeof(arr),sizeof(arr[0]));
}
int main() {
    int arr[SIZE][SIZE];
    size(arr);
    return 0;
}

輸出:

4 40
Process returned 0 (0x0)   execution time : 0.039 s
Press any key to continue.

分析
對於第二個輸出,應該是 40 這個大家都沒有什麼疑問的。
但是第一個是幾呢?
你是不是想着是 400 呢?
答案是 4.
這是因爲對於數組參數。第一級永遠是指針形式。
也就是說數組參數永遠是指針數組。
所以第一級永遠是指針,而剩下的級數由於需要使用 [] 運算符, 所以不能是指針。

6. size of參數

問題:
當我們有時候想讓代碼簡潔點的時候,會把運算壓縮到一起。
但是在 sizeof 中就要小心了。

代碼:

#include <stdio.h>
int main() {
    int i;
    i = 10;
    printf("i : %d\n",i);
    printf("sizeof(i++) is: %d\n",sizeof(i++));
    printf("i : %d\n",i);
    return 0;
}

輸出:

i : 10
sizeof(i++) is: 4
i : 10     //此處不是11
Process returned 0 (0x0)   execution time : 0.039 s
Press any key to continue.

分析:
sizeof 是一個關鍵字,但是在編譯器運算。
所以編譯器是不會進行我們的那些算術等運算的。
而是直接根據返回值推導類型,然後根據類型推導出大小的。

7. 位運算符左移

問題:
這個問題沒什麼說的,你運行一下就會先感到詫異,然後會感覺確實應該是這個樣字,甚至會罵這代碼寫的太不規範了

代碼:

#include <stdio.h>
#define PrintInt(expr) printf("%s : %d\n",#expr,(expr))
int FiveTimes(int a) {
    return a<<2 + a;   //此處先計算2+a
}
int main() {
    PrintInt(FiveTimes(1));
    return 0;
}

輸出:

FiveTimes(1) : 8
Process returned 0 (0x0)   execution time : 0.624 s
Press any key to continue.

分析:
需要我提示嗎?
三個字:優先級

8. 浮點數

問題:
大家經常使用 浮點數,知道背後的原理嗎?

代碼:

#include <stdio.h>
int main() {
    float a = 12.5;
    printf("%d\n", a);
    printf("%d\n", *(int *)&a);
    return 0;
}

輸出:

12.500000
1095237632
Process returned 0 (0x0)   execution time : 0.651 s
Press any key to continue.

分析:
爲了方便大家測試,我提供了一個32位浮點型向二進制的轉換器
首先 int 和 float 在 32位機器上都是 四字節的。
對於整數儲存,大家都沒什麼疑問。
比如 10 的二進制,十六進制如下

00000000 00000000 00000000 00001010
0X0000000A在這裏插入代碼片

由於最高位代表符號,所以整數可以表示的範圍就是

0X80000000 -2^31 
0XFFFFFFFF -1 負整數最小值
0X00000000 0
0X00000001 1 正整數最小值
0X7FFFFFFF 2^31-1 正整數最大值

上面的二進制也就決定了 4字節的整數範圍是 -2 ^ 31 到 2 ^ 31 - 1 .

對於一個浮點數,可以表示爲 (-1)^S * X * 2^Y .
其中 S 是符號,使用一位表示。
X 是一個 二進制在 [1, 2) 內的小數,一般稱爲尾數,用23位表示。
Y 是一個整數,代表冪,一般稱爲階碼,用8位表示。

其中 Y 又涉及符號問題了。
8位的Y可以表示0到255,取指數偏移值 127 (2^(8-1) - 1)作爲分界線,小於127的數是負數,大於的是正數。
這裏我不明白爲什麼不使用以前數字的表示方法。

比如 12.5 的二進制是 1100.1 。
轉化爲上面的公式就是 (-1)^0 * 1.1001 * 2^3

下面我們來推導一下這個數字的二進制是什麼吧。

符號爲正,所以第一位就是0了

3 + 127 就是 130 了。於是使用 10000010 可以表示。

1.1001 一般不表示小數前的1,於是只需要表示 1001 即可,於是使用 10010000000000000000000 就可以表示了。

於是 12.5 的 float 的二進制表示就推算出來了

0 10000010 10010000000000000000000
01000001 01001000 00000000 00000000

然後這個二進制對應着整數 1095237632 。
這樣一切都解釋清楚了。

當然還要注意一個問題,這裏有這麼一個特殊規定:階碼Y如果是0, 尾數X就不再加1了。

9 宏的定義

問題:大家都定義過宏吧,你的宏定義規範嗎?
代碼:

#include <stdio.h>
#define MUL(x,y) (x)*(y)
#define MUL1(x,y) x*y
#define LOG(msg) printf("line:%d\n",__LINE__);printf("msg:%s\n",msg)
int main() {
    int a = 2, b = 3, c;
    c = MUL(a+1, b+1);
    printf("%d * %d :mul = %d\n", a, b, c);
    c = MUL1(a+1, b+1);
    printf("%d * %d :mul1 = %d\n", a, b, c);
    c = MUL(a+1, b+1);
    if(c != 12)
        LOG("mul error");
    return 0;
}

輸出:

2 * 3 :mul = 12
2 * 3 :mul1 = 6
msg:mul error
Process returned 0 (0x0)   execution time : 0.039 s
Press any key to continue.

分析:
編程語言中所有的坑大部分都是代碼編寫不規範導致的。
比如使用隊列時,下面的語句你加大括號嗎?

while(!que.empty()){
    que.pop();
}

下面我們來看看上面的代碼爲什麼錯了,以及怎麼解決。
首先大家可能都清楚我們的宏變量都需要括號括起來。
比如 MUL, 如果不加括號, 就是 MUL1 了, 然後輸出變成 6 了。爲什麼是 6 呢?我們模擬一下展開過程

//a = 2, b = 3
MUL1(a+1, b+1)
a+1 * b+1 
a + b + 1
6

很好,不加括號確實應該是6.
那第二個爲什麼會輸出 msg:mul error 呢?展開後再格式化後我們可以看到是下面的樣子

if(c != 12)
    LOG("mul error");
=>
if(c != 12)
    printf("line:%d\n",__LINE__);printf("msg:%s\n",msg);
=>
if(c != 12)
    printf("line:%d\n",__LINE__);
    
printf("msg:%s\n",msg);

大家應該會想到需要在宏裏面加大括號,但是我們該如何加呢?
加之前大家可以先看看我們的宏定義

#define MUL(x,y) (x)*(y)
#define MUL1(x,y) x*y
#define LOG(msg) printf("line:%d\n",__LINE__);printf("msg:%s\n",msg)

我們的宏定義後面都缺少一個分號,爲什麼這樣做呢?
爲了滿足視覺上的合理性,即調用時我們往往會在後面加一個分號

c = MUL(a+1, b+1);
c = MUL1(a+1, b+1);
LOG("mul error");

好的,這個背景介紹完了,我們來看看加上大括號後的樣子吧。加大括號的時候肯定是先把分號補上了。

#define MUL(x,y) {(x)*(y);}
#define MUL1(x,y) {x*y;}
#define LOG(msg) {printf("line:%d\n",__LINE__);printf("msg:%s\n",msg);}

然後我們驚奇的發現竟然編譯不過去。
當然如果你編譯過去了,只是簡單的收到幾個警告,那說明你用的是最新版本的支持C++11的編譯器。
這裏假設你編譯不過去了。爲什麼編譯不過去呢?我們把第一個宏展開看看是什麼吧。

c = {(a+1)*(b+1);};

天呢,這是什麼東西,顯然是有問題的。

好吧,這個問題我們沒辦法解決了。
在我們不知道怎麼辦的時候,我們意外的發現 LOG(“mul error”); 竟然編譯過去了。
爲什麼呢? 再次展開一下

if(c != 12)
    LOG("mul error");
=>
if(c != 12)
    {printf("line:%d\n",__LINE__);printf("msg:%s\n",("mul error"));};
    
=>
if(c != 12){
    printf("line:%d\n",__LINE__);
    printf("msg:%s\n",("mul error"));
}
;

好吧,除了最後多了一個分號空語句,其他的地方都是完美的。
但是這個可惡的分號會影響 else 語句的。

if(c != 12)
    LOG("mul error");
else
    printf("ok\n");
=>
if(c != 12){
    printf("line:%d\n",__LINE__);
    printf("msg:%s\n",("mul error"));
}
;
else
    printf("ok\n");   

這個時候又會編譯不過去的。
但是這個錯誤是由於我們的代碼編寫不規範導致的,我們通過加大括號可以解決。

if(c != 12){
    LOG("mul error");
}else{
    printf("ok\n");
}
=>
if(c != 12){
    {
        printf("line:%d\n",__LINE__);
        printf("msg:%s\n",("mul error"));
    }
    ;
}else{
    printf("ok\n"); 
}

雖然加大括號後展開的代碼看着比較奇怪,但是最起碼我們暫時解決了一個問題。
你也知道,大部分程序員都是有潔癖的。
因此怎麼能容忍這麼醜陋的代碼存在呢?
於是我們需要尋找看起來比較優美的展開代碼。
程序員的智慧是無限的,還真找到兩個來。

#define FOO(X)   do { something;}   while (0)
#define FOO(X)   if (1) { something; }   else

上面兩個代碼就是比較優美的代碼,展開後是這個樣子

#define LOG(msg) do {printf("line:%d\n",__LINE__);printf("msg:%s\n",msg);} while (0)
if(c != 12){
    LOG("mul error");
}else{
    printf("ok\n");
}
    
=>
if(c != 12){
    do {
        printf("line:%d\n",__LINE__);
        printf("msg:%s\n",msg);
    }  while (0);
}else{
    printf("ok\n");
}

#define LOG(msg) if (1) { printf("line:%d\n",__LINE__);printf("msg:%s\n",msg); } else
if(c != 12){
    LOG("mul error");
}else{
    printf("ok\n");
}
    
=>
if(c != 12){
    if (1) { 
        printf("line:%d\n",__LINE__);
        printf("msg:%s\n",msg);
     } else ;
}else{
    printf("ok\n");
}

上面兩個代碼看起來優美多了,而且 do{}while(0) 用的更多一些,畢竟 else ; 看着還是有那麼一點不舒服。
但是優美歸優美,我們還是需要把所有問題解決了纔算真正的優美。
那對於 這個醜陋的代碼 {(a+1)*(b+1);}; 到底該如何解決呢?
這麼醜陋的代碼是由這個語言本身的語法導致的,所以只好從語言本身上解決了。
簡單的說使用目前的方法沒辦法完美解決,即定義一個規則不能滿足所有情況,於是官網提供了一個新的語法
c++11 定義了新的語法:

#define MUL(x,y) ({(x)*(y);})
c = ({(a+1)*(b+1);});

看到了什麼?

簡單的說就是對宏語句加個大括號,然後大括號外加個小括號。最後一個值作爲返回值。
這和很多解釋性語言的函數類似,最後一條語句的返回值作爲函數的返回值。
這個問題頗爲複雜,不過前端時間我寫了這麼一篇文章 宏 do{}while(0)., 感興趣的可以看一看。http://github.tiankonguse.com/blog/2014/09/30/do-while.html
看到這,有些人可能會感覺有點不對勁,但是那裏不對勁有說不上來。
於是再看一遍,找到原因了:作者在誤導人啊。爲什麼呢?
還記得上面加大括號的時候我們是往宏上加的,有的人可能會說我們先把代碼寫規範了,在看看會有那些問題唄。

#include <stdio.h>
#define MUL(x,y) ((x) * (y))
#define LOG(msg) printf("line:%d\n",__LINE__);printf("msg:%s\n",msg)
int main() {
    int a = 2, b = 3, c;
    c = MUL(a+1, b+1);
    printf("%d * %d :mul = %d\n", a, b, c);
    if(c != 12) {
        LOG("mul error");
    }
    return 0;
}

理想情況下,代碼先寫規範了, 發現什麼問題都沒有。
但是現實和理想還是有差距的。細心的人可能發現我的 MUL 這個宏和上面的宏還是有點區別的, 最外面又加了一個小括號,這也是一坑。那究竟什麼樣的代碼纔是規範的呢?這個不好說,因爲一個語言的所有細節都要整理出來的話,那將會是一個很大的文檔。
所以目前這些只好靠經驗,閱讀書籍,看別人的文檔,一個坑一個坑的踩之後才能慢慢的瞭解這些細節。
如果誰知道有這麼一個文檔的話,可以留言告訴我,我只知道有一個簡單的文檔 Google C++ Style Guide

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章