1. 程式人生 > >淺談程式碼規範&&基礎除錯&&幾道面試題

淺談程式碼規範&&基礎除錯&&幾道面試題

廢話篇:本文由CSUST的FINAL實驗室的LX創作,用途是給予CSUST的小鮮肉們一些關於C語言程式碼規範的一些基本知識,若本文有什麼錯誤或是表述不清之處,歡迎留言討論指正。

程式碼規範:

在講程式碼規範之前,我想給大家看一句感人肺腑的註釋名言來告誡各位以後的優秀程式猿:

//When I wrote this, only God and I understood what I was doing.
//Now, God only knows.

在寫第一行程式碼前,我希望大家思考一個問題,在第一行我們應該寫什麼?我相信大部分的初學者都會寫:

#include <stdio.h>
///來一個有獎問答題
///這個標頭檔案stdio的意義是什麼?(提示:可以分成三個單詞的簡寫)

有問題嗎?答案是沒有的。這樣寫的確是沒有任何問題,但是。。。不夠騷氣。開個玩笑,不過我還是希望有人會喜歡在開始會寫上註釋,來表明這些程式碼的大概內容,作者,版權所有翻版必究(霧)

在C語言中基本上有兩種註釋方式如下

//單行註釋
//每次換行都要在最前面加上//

/*多行註釋
巴拉巴拉
以這個結尾->*/

可以看到一種是單行註釋,即每行註釋前都要在最前方加上//,換種說法就是如果在最前方看到了//,無論這行的內容有什麼,都屬於註釋範疇不會被編譯。

第二種是多行註釋,又稱作塊註釋,這樣就可以愉快的在/*與*/間加入你想要寫的任意註釋,也就是在這塊區域裡的內容都回在編譯時略過。

然而我想說,上面這種多行註釋,事實上並不夠騷(霧),事實上根據程式碼規範,應該像下面這樣寫:

/* 以"/* "開頭
 * 中間的註釋以" * "開頭
 *///以" */"結尾(之所以這一段是 "*///"開頭的原因是因為"*/"之後的就不算作註釋內容了)

但是要注意,多行註釋不能巢狀,例如下面這樣:

/* 
    巴拉巴拉
    /*
        巴拉巴拉
    */
    巴拉巴拉
*/

很明顯,編譯器把第二個/*當成了註釋來對待,當遇到了第一個*/時,便認為第一個*/對應的是第一個/*,而中間的/*是註釋。這樣第二個*/找不到一個匹配的/*,所以存在問題。

學了這些這時候我們可以試著寫出一個比較像樣子的程式碼註釋了:

/* 
 * 標題:C程式碼規範
 * 作者:CSUST LX
 * 巴拉巴拉
 * 日期:2018/8/10
 * 本文意義:巴拉巴拉
 */

這樣你就可以愉快的和你那沒看過這種酷酷的註釋的室友裝逼了,是不是很棒呀?

當然註釋的意義不是為了裝X,在實際工程中,一個大型專案可能會由很多人接手,或者隨著軟體的更新換代,程式碼會交給其他的同事去使用、學習或者是改進,如果你寫的亂七八糟又沒有註釋,或者這地方邏輯很繞卻沒有註釋等,會讓你的合作者感到十分困惑且痛苦。除此之外,對於自己而言,雖然說是自己寫的程式碼,但是過了很久之後難免會忘記,因此寫一個註釋來概括程式碼的作用、解釋某個函式的引數作用以及函式的意義等是十分有意義的。後還是會有很大的好處的。

舉個例子,大家可以看一下下面這段程式碼:

size_t Func(const char *str)
{
        const char *char_ptr = NULL;
        const unsigned long int *longword_ptr = NULL;
        register unsigned long int longword, magic_bits;
 
        for (char_ptr = str; ((unsigned long int)(char_ptr) 
                        & (sizeof(unsigned long int) - 1)) != 0; ++char_ptr)
        {   
                if (*char_ptr == '\0')
                        return char_ptr - str;
        }    
 
        longword_ptr = (const unsigned long int *)char_ptr;
        magic_bits = 0x7efefeffL;
 
        for ( ;; )
        {   
                longword = *longword_ptr++;
                if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0)
                {   
                        const char *cp = (const char *)(longword_ptr - 1); 
                        if (cp[0] == '\0')
                                return cp - str;
                        if (cp[1] == '\0')
                                return cp - str + 1;
                        if (cp[2] == '\0')
                                return cp - str + 2;
                        if (cp[3] == '\0')
                                return cp - str + 3;
                }   
        }   
}

給大家看半分鐘,我相信,大家還是一頭霧水吧。不過,我也是看的一頭霧水呢!

不過如果我給你看的是下面這段程式碼呢?

///功能   :返回一個字串的長度
///引數str:要求長度的字串的首地址
size_t Func(const char *str)
{
        const char *char_ptr = NULL;
        const unsigned long int *longword_ptr = NULL;
        register unsigned long int longword, magic_bits;
 
        for (char_ptr = str; ((unsigned long int)(char_ptr) 
                        & (sizeof(unsigned long int) - 1)) != 0; ++char_ptr)
        {   
                if (*char_ptr == '\0')
                        return char_ptr - str;
        }    
 
        longword_ptr = (const unsigned long int *)char_ptr;
        magic_bits = 0x7efefeffL;
 
        for ( ;; )
        {   
                longword = *longword_ptr++;
                if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0)
                {   
                        const char *cp = (const char *)(longword_ptr - 1); 
                        if (cp[0] == '\0')
                                return cp - str;
                        if (cp[1] == '\0')
                                return cp - str + 1;
                        if (cp[2] == '\0')
                                return cp - str + 2;
                        if (cp[3] == '\0')
                                return cp - str + 3;
                }   
        }   
}

是不是感覺瞬間知道了這個函式是怎麼用了呢?雖然我看不懂,但是我會用鴨!這種情況下,想想如果你的公司的同事寫程式碼不加註釋的話,你是不是會動了殺意呢? 

另外,如果這段程式碼是這樣的呢?你是否會恍然大悟?

size_t strlen(const char *str)
{
        const char *char_ptr = NULL;
        const unsigned long int *longword_ptr = NULL;
        register unsigned long int longword, magic_bits;
 
        for (char_ptr = str; ((unsigned long int)(char_ptr) 
                        & (sizeof(unsigned long int) - 1)) != 0; ++char_ptr)
        {   
                if (*char_ptr == '\0')
                        return char_ptr - str;
        }    
 
        longword_ptr = (const unsigned long int *)char_ptr;
        magic_bits = 0x7efefeffL;
 
        for ( ;; )
        {   
                longword = *longword_ptr++;
                if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0)
                {   
                        const char *cp = (const char *)(longword_ptr - 1); 
                        if (cp[0] == '\0')
                                return cp - str;
                        if (cp[1] == '\0')
                                return cp - str + 1;
                        if (cp[2] == '\0')
                                return cp - str + 2;
                        if (cp[3] == '\0')
                                return cp - str + 3;
                }   
        }   
}

你們應該發現了吧!這個其實是標頭檔案<string.h>裡的一個函式strlen,大家應該多多少少接觸到過,這個函式名字是string(字串)和length(長度)的縮寫結合,有這樣一個函式名的話,是不是感覺看起來就很容易理解呢?

下面給大家幾個函式名大家來猜猜是幹什麼的:

int StrToInt(char strData[]);

Girlfriend CreatAGirlfriend();

int find_max_value(int data[]);

從上面的命名裡,我們看到了三種常見的命名規範:

1. 駝峰命名法:駝峰命名法就是當變數名或函式名是由一個或多個單詞連結在一起時,第一個單詞以小寫字母開始;從第二個單詞開始以後的每個單詞的首字母都採用大寫字母。(例如:myName,hisBoyfriend)

2. 帕斯卡命名法:帕斯卡命名法就是當變數名或函式名是由一個或多個單詞連結在一起時,每個單詞的首字母都採用大寫字母。(例如:MyName,HisBoyfriend)

3.下劃線命名法:所有字母均小寫,每個單詞間以下劃線分割。(例如:my_name,his_name)

在使用上,這樣的命名規範都十分清晰明瞭。具體而言,我們一般給函式名、全域性變數和結構體名等通用性強的採用帕斯卡命名法,區域性變數、函式形參採用駝峰命名法或下劃線命名法。

總之,取這些名字的意義是:讓自己的程式碼更便於理解閱讀。

所以請以後寫程式碼的時候,儘量給自己的變數和函式等起一個合理的名字。

下面我們開始講點程式碼規範。

#include <stdio.h> //程式碼規範:在include與<間留一個空格
/* 
 * stdio的意思 std是standard的縮寫,i是input的縮寫,o是output的縮寫
 * h是head的縮寫
 * 所以stdio是標準輸入輸出標頭檔案的意思
 */
int main()
{
    return 0; //在返回值為int的情況下記得加return 0;養成好習慣(有些題目要求了main函式返回值必須為0,否則算錯
}

下面來一段寫的看不下去的程式碼,大家來找找茬。

#include<stdio.h>
 int main()
{double a,b;
scanf("%lf%lf",&a,&b);
if(a>b)
printf("%lf",a);
else printf("%lf",b);
return 0;}

這段程式碼真的是讓人無法忍受的,這樣的程式碼層次特別不清晰。如果你問一個學長學姐的時候,你的程式碼是這樣的話,他/她可能鳥都不想鳥你。

正常點的話,程式碼應該是下面這樣的:

#include <stdio.h>         ///空格最好加上
int main()                 ///nt前面不要加空格
{                          ///每出現一個'{'後代碼要向後縮排直到遇到'}',語句不要直接放在'{'後面
    double a,b;
    scanf("%lf%lf",&a,&b);
    if(a>b)
    {
        printf("%lf",a);
    }
    else                   ///像if,else這類後面要接語句的關鍵詞,後面最好加複合語句,以方便以後修改
    {
        printf("%lf",b);
    }
    return 0;
}

但是如果你想只想大體上可以讓別人看的順眼的話,下面用Codeblocks編譯器舉個栗子:

這就是剛剛那段難以置信的程式碼。這時候請你趕緊對著文字框裡按右鍵別瞎了眼睛。

這時候你會發現一個讓你難以置信的無敵法寶——Format use AStyle!!!

他可以一下就讓你的程式碼變這樣:

這樣一來,就很方便的解決了大部分的程式碼規範問題,這時候你才有臉去找學長學姐問問題。

但是千萬不要濫用這個,程式碼規範要自己寫的時候注意好,不要寫了一堆亂七八糟的程式碼後再去點那個,為什麼要辣自己的眼睛呢?眼睛做錯了什麼?

但是如果,你要是用的不是Codeblocks呢?

這樣你是不是要等著我像告訴一個小寶寶一樣告訴你呢?

當然是選擇!!!!

這兩個可愛的網站難道你們不會使用嗎?你們難道不是經歷了九年義務教育的同學麼,有問題麼?當然選擇去查啦!!!

除錯篇:

除錯是一個程式猿的基礎能力,學了這個之後,希望各位發現問題了之後別第一時間想到去問別人,想想辦法自己去DEBUG吧~

後面的程式碼有包含函式的內容,如果有小同學還沒有學到這裡的話我就稍微提一下:

例如下面的程式碼:

#include<stdio.h>
int main()
{
    printf("倒計時:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");
    printf("何暢大佬女裝登場!\n");
    printf("倒計時:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");
    printf("何暢大佬和你們揮手說再見!\n");
    printf("倒計時:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");
    printf("再見!何暢女裝大佬!\n");
    return 0;
}

我們發現,這段程式碼中有很多重複的程式碼。總結下來的話,主要是倒計時那一段:

    printf("倒計時:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");

我們為了簡化程式碼,增加程式碼的複用性,採用了一個叫做CountDown(倒著數)的函式。這個CountDown就是我們給這個函式自定義起的函式名。

#include<stdio.h>
void CountDown()
{
    printf("倒計時:\n");
    printf("3\n");
    printf("2\n");
    printf("1\n");
    printf("...\n");
}

int main()
{
    CountDown();
    printf("何暢大佬女裝登場!\n");
    CountDown();
    printf("何暢大佬和你們揮手說再見!\n");
    CountDown();
    printf("再見!何暢女裝大佬!\n");
    return 0;
}

接著考慮下,如果我們現在要從4開始倒數,我們要怎麼辦?再寫一個函式?還是說我們可以找到一個辦法可以更好的解決這個問題呢?

答案是:用函式形參和迴圈。

void CountDown(int n)
{
    printf("倒計時:\n");
    for(int i=n;i>=1;i--)
    {
        printf("%d\n",i);
    }
    printf("...\n");
}

這裡我們可以看到括號裡那個n,其實就是我們所稱的形參,形參的話是沒有實際值的,只有在呼叫這個函式的時候你給它指定值,那個值不管是變數還是常量都稱之為實參。

然後讓我們呼叫這個函式:

#include<stdio.h>
void CountDown(int n)
{
    printf("倒計時:\n");
    for(int i=n;i>=1;i--)
    {
        printf("%d\n",i);
    }
    printf("...\n");
}

int main()
{
    int a;
    scanf("%d",&a);
    CountDown(a);
    printf("何暢大佬女裝登場!\n");
    CountDown(a);
    printf("何暢大佬和你們揮手說再見!\n");
    CountDown(a);
    printf("再見!何暢女裝大佬!\n");
    return 0;
}

我們發現,這個程式裡,CountDown函式的形參是n,而呼叫這個函式的時候實參是a,但是仍然可以成功執行,說明函式的形參和傳進來的實參並沒有關係。

為了說明這點,我先來說一下變數的作用域:



我們把程式碼拖去執行會發現。。。

執行不了!

因為a沒有進行初始化啊!!!

所以把相關程式碼去掉後接著來看:

#include <stdio.h>

int tmp;

int main()
{
    int a,i;
    printf("tmp=%d\n",tmp);
    a=5;
    printf("賦值後a=%d\n",a);
    for(i=0;i<2;i++)
    {
        int a=i;
        printf("%d\n",a);
    }
    printf("%d\n",a);
    return 0;
}

 我們發現,a在被賦值了之後,在迴圈中,被賦值成了0和1,退出迴圈後又變為了之前的5。這個現象說明了什麼呢?

就是迴圈中的那個a和之前所定義的那個a所處的記憶體位置不一樣。

為了驗證我們的想法,我們用下面這段程式碼:

#include <stdio.h>

int tmp;

int main()
{
    int a,i;
    printf("tmp=%d\n",tmp);
    a=5;
    printf("main函式裡的a的地址為:%d\n",&a);
    for(i=0;i<2;i++)
    {
        int a=i;
        printf("for迴圈裡的a的地址為%d\n",&a);
    }
    printf("%d\n",a);
    return 0;
}

你會很神奇的發現,他們的地址還真的不一樣。

這說明了一點:當一個巢狀結構中出現兩個不同作用域的變數時,變數的名稱可以相同,在使用時以其小作用域為準。

所以這些相同變數名的不同變數的來源就是作用域不相同這個原因。

於是我們看下面這個栗子:

#include <stdio.h>

void Change(int a ,int b)
{
    int c=a;
    a=b;
    b=c;
}

int main()
{
    int a=3,b=5;
    Change(a,b);
    printf("%d %d",a,b);
    return 0;
}

我們會發現,a和b的值,呼叫了Change函式之後並沒有發生改變,聰明的你一定想到了是定義域不同的原因,所以函式裡的a不是main函式裡的a。 

但是,如果我執意要交換a和b的值呢?

那當然是選擇放棄啊。。。

當然不是,這時候就有一個叫指標的好東西來了。

指標是什麼呢?它事實上是一個數字,代表著一個地址。

指標是怎麼玩的呢?首先我們在學習用scanf函式的時候,我們學到了一個叫做&取地址符的符號,用這個單目運算子可以得到變數的地址,於是我們來試一下。

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

 執行結果大概是這樣的,不過p_a的值可能你會和我的不一樣。

我們驚奇的發現,p_a裡面儲存的就是&a的數值,而且對於char和int,他們所佔的記憶體空間大小不同,但是他們的指標的大小卻是一模一樣,這是因為在相同的環境(編譯器和系統都一樣)下,不同型別的指標的大小都是一樣的。

這個地址呢,就好像是一個房間號。當我們使用普通傳參呼叫函式的時候,電腦會把實參的值賦值給形參,相當於就是何暢學長在213號房間坐著,他男朋友去找他,但是他男朋友的做法是:在旁邊的214號房把213號房間複製了一份,裡面的花花瓶瓶都是一模一樣的,然後他就坐在裡面。這樣很明顯是錯誤的,因為他們兩個不在一個房間裡,當然就不能很愉快的玩耍了。

然鵝,如果我們使用指標呢?我們想起來了,原來指標是表示地址的,那麼,現在何暢學長在213號房的話,他男朋友就可以開心的直接去找他了不是?

於是我們用指標來改寫上面的程式碼:

#include <stdio.h>

void Change(int* a ,int* b)
{
    int c=(*a);
    (*a)=(*b);
    (*b)=c;
}

int main()
{
    int a=3,b=5;
    Change(&a,&b);
    printf("%d %d",a,b);
    return 0;
}

這樣我們的執行結果就是下面這樣了:

我在網上居然找到一個關於codeblocks的斷點除錯的部落格,這個時候!

當然是選擇偷懶啦,你們自己點進去看看吧:當然是選擇偷懶鴨不過裡面的樣例是個C++程式,你們現在可能還看不懂,那麼也可以參考下我後面舉的栗子,其他的操作細節的話你們可以參考上面我給你們的部落格。

是不是很簡單都覺得很無聊了?咱們接著講除錯栗子。

新建什麼的咱們就不管了。直接上程式碼:

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

///復讀str字串n次
void Repeater(char str[],int n)
{
    int i;
    for(i=0;i<n;i++)
    {
        printf("%s\n",str);
    }
}

int main()
{
    int n;
    scanf("%d",&n);
    char str[] = "ouyanghaoxuezhangzhenniubi";
    Repeater(str,n);
    return 0;
}

沒錯就是這段程式碼啦~

有沒有想過寫一個復讀機呢?雖然上面的程式碼寫的也不是一個復讀機,我們就假裝它是好了,畢竟裡面有個函式叫做Repeater呢。

我們複製到Codeblocks裡新建的工程後,大概是長這個樣子:

然後我們就可以很愉快的點選行數和黃線中間的白色區域給指定的行數新增斷點,或者是其他什麼方法都行,這個方面有什麼問題看剛剛那個部落格,強調!

愉快的瞎點了幾行之後,你會發現居然變這樣了:

很明顯,那些紅點點不是什麼好傢伙,一看就是剛剛你點的斷點,但是這個到底有什麼用呢?

我們找到上面的Debug->Start/Continue,點選這個。

你會發現出現了一個黑框框:

欸?你說你沒看到這個框框嗎?有獎問答,這時候要怎麼辦?

接下來我們輸入3,也就是讓n=3。

等下!這裡好像發生了一件不得了的事情,為什麼程式沒有結束執行?而且這兒一個紅點點上怎麼出現了一個黃色三角形?

聰明的小夥伴肯定就會想到了,哦,原來是執行到這裡就停止了鴨,這就是斷點的作用!

這時候我們點選Debug->Debugging windows->Watches開啟變數觀察框(Watches)。

這時候我們發現n=3了,而str卻很奇怪。

我們點選下Debug->Next line

你就會發現Watch視窗變成這樣了:

你發現了,str變成了“ouyanghaoxuezhangzhenniubi”。但是,i卻變得很是奇怪。

來吧,讓我們看一下程式碼~

讓我們來一起想想想~

結論是:一條打了斷點的語句,會在執行到這句語句之前停止。直到你手動去讓程式執行。

接著執行:

我們發現我們停留在printf語句裡,黑框框裡沒有輸出,n變成了3。和你想的一樣嗎?

來,猜一下執行下一條語句會是怎樣的。

結果是:

由於這是個迴圈語句,所以我們又返回到了for語句那裡,發現i==0,所以i++還沒有執行。

而黑框框裡:

發現有輸出,說明迴圈進行了一次了。

接下來的驗證操作就交給大家吧,然後我們準備關閉除錯。

點一下,你就完成了我們的Debug操作。

正所謂是:點一下,De一年,Bug不花一分錢。

什麼?到現在你還不知道Debug有什麼用?我們可以通過斷點讓程式在我們想觀察的點停下來,然後我們可以通過Watch窗口裡變數的值和我們心裡對該程式執行時該變數的理論上的值比對,從而推斷出我們的程式是哪兒出錯了。

搞了半天,還是要用腦子的啊!

某面試題的講解

首先,我是不會給你們講解太難的那種題的,碰到了的同學,只能怪你自己了,誰叫你這麼強呢?

1.判斷一個正整數是否能被2整除:

關鍵程式碼:n%2==0

然鵝,這樣一點也不騷啊。

讓我們來想想2進位制下的世界:

1 : 0001
2 : 0010
3 : 0011
4 : 0100
5 : 0101
6 : 0110
7 : 0111
...

發現了什麼嗎?好像只要是2的整數倍的數字,最後一位一定是0。

接下來我們介紹下位運算。相信各位接受了九年制義務教育的優秀大學生肯定對邏輯運算是有所瞭解的啦。相必&&符號大家都知道,這個符號是當左右兩邊都不為0的情況下返回1,否則返回0的一個運算子。在位運算裡,有一個&符號,這個是對於數的二進位制的每一位來進行運算的,舉一個栗子:

01 & 01 = 01
00 & 01 = 00
10 & 00 = 00
10 & 11 = 10
11 & 11 = 11

這樣我們發現,這個&運算相當於就是邏輯運算與的二進位制版。

& 按位與運算    兩個位都為1時,結果才為1
| 按位或運算    兩個位都為0時,結果才為0
^ 按位異或運算  兩個位相同為0,相異為1
~ 按位取反運算  0變1,1變0

知道了這些的話就很簡單了。

我們發現對於每個可以被二整除的數字來說,最後一位都是0,所以我們只要考慮最後一位。

所以我們對一個數與1進行或運算,舉例如下:

在4bit的情況下,1的二進位制是0001。
若n = 4為偶數的話,它的二進位制是0100。
0100 & 0001 = 0000
若n = 3為奇數的話,它的二進位制是0011。
0011 & 0001 = 0001
這時候我們發現,與1做按位與運算後,奇數變成了1,偶數變成了0。

於是我們寫個稍微騷點的程式碼:

#include <stdio.h>
int main()
{
    int n;
    scanf("%d",&n);
    if(n&1==1)
    {
        printf("%d是奇數",n);
    }
    else {
        printf("%d是偶數",n);
    }
    return 0;
}

接著如果你答上來了的話,我一般會問:

2.判斷一個正整數是不是2的n次方。

我們來觀察一下2的n次方的二進位制的規律。

2^0    0001    1
2^1    0010    2
2^2    0100    4
2^3    1000    8

我們發現了,如果這個數是2的n次方的話,這個數的二進位制裡肯定只有一個1。

所以我們可以通過將這個數字轉換成2進位制然後統計它裡面有多少個1來判斷這個數是不是2的n次方。

但是這樣還是不夠騷啊!那要怎麼辦?

我們讓這些數減1看看。

2^0-1    0000    0
2^1-1    0001    1
2^2-1    0011    3
2^3-1    0111    7

我們驚奇的發現,除了原來的1變成了0以外,1後面的所有0都變成了1。

Image result for ææä¸ä¸ªå¤§èçæ³æ³

我們來進行一下&運算看看:

0001 & 0000 = 0000
0010 & 0001 = 0000
0100 & 0011 = 0000
1000 & 0111 = 0000

居然全都是0!

那麼不是2的n次方的數字與它減一後的數進行&運算後,會怎樣呢?

讓我們來想一下,2^n的條件就是,二進位制裡只有一個1,反之,出現了1個以上1的數字就不是2^n次方。

那麼,也就意味著,在最前面的1的後面,至少也存在一個1,減了1之後,它的最前面那位肯定不會變成0的,也就是說,最前面的那位1是一定不會變化的,於是進行了與運算後一定不為0。

程式碼如下:

#include <stdio.h>
int main()
{
    int n;
    scanf("%d",&n);
    if(n&(n-1)==0)
    {
        printf("%d是2的n次方\n",n);
    }
    else {
        printf("%d不是2的n次方\n",n);
    }
    return 0;
}

讓我們執行一下,輸入一個2,大家想想會輸出什麼?

怎麼樣?

因為按位運算子的運算優先度不高,所以首先會運算(n-1)==0,1!=0,所以返回的是0。

2&0=0,所以會去執行else裡面的語句。

因此事實上應該這麼寫:

#include <stdio.h>
int main()
{
    int n;
    scanf("%d",&n);
    if((n&(n-1))==0)
    {
        printf("%d是2的n次方\n",n);
    }
    else {
        printf("%d不是2的n次方\n",n);
    }
    return 0;
}

希望大家能夠從這節課裡多多少少學到點什麼,不說了,待會何暢大佬要提刀來見我了,請各位給點面子保護下我。