• Moegirl.ICU:萌娘百科流亡社群 581077156(QQ),欢迎对萌娘百科运营感到失望的编辑者加入
  • Moegirl.ICU:账号认领正在试运行,有意者请参照账号认领流程

用戶:渚 花/C語言教程

萌娘百科,萬物皆可萌的百科全書!轉載請標註來源頁面的網頁連結,並聲明引自萌娘百科。內容不可商用。
跳至導覽 跳至搜尋
Icon-info.png
剛剛我發現B站一些視頻教學比我寫的這個好得多了,所以棄坑。但這個頁面還會保留

--渚_花(討論) 2023年10月5日 (四) 22:08 (CST)


Icon-info.png
建議使用電腦觀看,以獲得更好的閱讀效果。你見過哪個程式設計師用手機看代碼和碼代碼的
#include <stdio.h>
#include <stdlib.h>
int main(){
  printf("Hello world!\n");
  getchar();
  return 0;
}

上面是C語言的hello world代碼。如果你是初學者的話,作為學習第一門編程語言的儀式,先嘗試一下執行以下上面的hello world代碼吧。具體網上有大量教程。

初學者學習一門任何一門新語言的第一個程序,都是在命令行上打印hello world,這是一個傳統。因此也有下面這個編程笑話

某程式設計師對書法十分感興趣,退休後決定在這方面有所建樹。於是花重金購買了上等的文房四寶。一日,飯後突生雅興,一番磨墨擬紙,並點上了上好的檀香,頗有王羲之風範,又具顏真卿氣勢,定神片刻,潑墨揮毫,鄭重地寫下一行字:hello world
——https://zhuanlan.zhihu.com/p/603433946

推薦初學者使用VSCode或者Codeblocks電腦的這兩個軟件。另外,如果是使用VSCode的話。推薦使用Code Runner插件,這個插件可以省去大量的配置工作

  • 為什麼這個hello world代碼和其他教程的不一樣?這個下面第零章內容會詳細講

如果報錯了怎麼辦?因為上面的代碼太基礎了,所以會有很多初學者遇到和你一樣的錯誤。解決方法是,把錯誤信息複製下來,百度

  • 學習計算機電腦的科學一定要學會使用百度,對於一些提問類的內容可以使用知乎。不要迷信一些「stackoverflow比百度好」等。簡單的問題百度就可以解決
暈字怎麼辦
  • 把文段逐個字閱讀一遍,就可以當做把這一段學過一遍了。因為這些知識本身有難度,所以只學一遍不懂很正常。可以只先看一遍了解個大概,得到弄不懂的時候再細看就可以
  • 我認為學習任何東西都是一樣,從開始一點不懂,到稍微懂一點,到帶懂不懂,再到最後使用的得心應手,是一個逐步增長的過程。我認為並不存在一種學習方法能讓人一次性從一點不懂到完全弄懂
    • 有這樣一個笑話,一個人吃了一個三個包子,吃了第三個包子飽了,然後說「我真傻,如果我只吃第三個包子就好了」。我認為學習也是一樣,你可能看某個教程之後豁然開朗,這可能並不是因為這個教程寫得好,而是因為你在之前看過其他教程的原因
  • 可以把文段當做符號遊戲,比如下面的文段,要注意死死盯住它們的邏輯關係
文段

編譯器將C語言代碼編譯可執行文件,可執行文件內部是可以直接由CPU執行的機器碼。機器碼可讀性很差,所以通常將一些機器碼簡記為匯編。匯編有兩種風格,分別是IntelAT&T

    • 這一段提到這些符號,編譯器C語言代碼編譯可執行文件CPU機器碼可讀性簡記匯編風格IntelAT&T
  1. 編譯器這個東西將C語言代碼這個東西進行一個叫做編譯的操作,然後得到一個叫可執行文件這麼個東西
  2. 可執行文件這個東西的內部是一個叫做機器碼的東西,這個叫做機器碼的東西可以被CPU進行名字叫做執行這麼個操作
  3. 有一個叫可讀性的東西,具有好差之分,可以用來評價機器碼
  4. 因為機器碼有一個可讀性很差這麼個屬性,所以機器碼通常被大家用簡記這個操作,機器碼被簡記得到的東西叫匯編
  5. 匯編按照風格這個東西可以分為兩種匯編,這兩種一個叫Intel這麼個名字,一個叫AT&T這麼個名字
  • 你會發現,你完全不需要知道上述名詞的意思,也可以弄明白它們的邏輯關係
  • 未來你也許會發現,相對於閱讀寫起來非常囉嗦的文段,還是閱讀寫起來簡潔而信息量大的文段更加高效

緒言

本教程將嘗試以用一些與其他教程不同的方式,比如將常量運算和變量運算分離等,解決大量新人難以理解的問題,比如很多新人到了指針之後就完全學不懂的問題

本教程的示例代碼較少,幾乎沒有習題和作業

第零章 入門

為什麼學習編程語言都要以hello world入門呢?這是因為每門編程語言的hello world程序,都「麻雀雖小五臟俱全」,裏面攜帶着這門編程語言大量的語法信息

這一章開始會出現大量的專有名詞。其中很多第一次出現的專有名詞後面會詳細解釋,但有一些名詞並不會給出解釋,需要讀者自己通過語境進行理解。給出大量專有名詞一是為了給讀者一種「C語言是一個好大的開放世界」的探索的感覺,二是為了方便讀者遇到問題時進行表述,期望解決「我遇到了某個問題不知道怎麼描述」的現象。這裏很多內容只需要熟悉,而不需要完全弄懂

為什麼是第章?這是因為程式設計師計數都是從0開始計數的。比如C語言的數組,C++的vector,Python的列表中最開始的元素的下標是0而不是1,儘管lua是個例外

  • 學習計算機科學的時候很多時候是這樣,只需先大概了解一些內容和原理,大概了解一些專有名詞,待到需要或感興趣時再細了解即可
    • 計算機科學的知識太多,是學不過來的,且新知識產出的速度很可能會大於你學習的速度。但只需要學習的科學和技術可以用其幫助其他人,解決其他人的需求,就可以為社會做貢獻了
      • 至於怎麼知道人們需求什麼,可以通過觀察下自己需求什麼,從了解自己和自己的需求開始
      • 但是需求也分能滿足的需求不能滿足的需求兩種,所以需要區分
    • 如果想實際應用計算機科學的知識到實踐,你會發現一些知識只需要了解大概原理,甚至無需了解,只需調用前人程式設計師設計好的接口即可
      • 某種層面來說,程式設計師是一個自掘墳墓的職業。這個東西就像發現新大陸一樣,如果地圖上所有的內容都被發現完了,那麼後來的探險者探險就沒有拓展的意義了
    • 如果你想發展計算機理論,那麼如果你希望你的研究成果能落地被實際應用,那麼你至少需要調查程式設計師需要發展什麼樣的理論,或者根據自己的需求來發展理論(因為你遇到的問題,別人很可能也遇到過)
      • 但是需要注意的是,計算機科學的一個共識是不要重複造輪子。當然可以做小程序做着玩或者達到練習目的,但如果自己做出來的輪子沒有比前人的輪子達到一定的優勢,那麼不要想着要取代前人的輪子
        • 因為很多好用的輪子已經得到了大量使用,如果替換一個新的輪子取代舊輪子需要大量替換工作
  • 計算機科學相對於其他很多科學,歷史並不長。但是計算機科學是人類智慧的結晶
  • 前人的艱辛,你至少都要重來一遍。也許科學的發展就是這樣,雖然高峰高不可攀,但每一代人都在用自己的生命為它的發展貢獻或多或少的部分,然後後人踩着前人的屍體作為台階,再後來的人把我們作為台階

きみ2出会であ2ため0まれ01ただとか
きみ2まも2ため0この01ささ2とか
我是為你而生的嗎 有為你獻出此生嗎

——Patricia花開物語印象曲

你好世界

#include <stdio.h> // 每一行//后的内容是注释,注释不会对代码执行结果产生影响
#include <stdlib.h> // 上面一行引用了一个名字叫做`stdio.h'的头文件,这一行引用了一个名字叫做`stdlib.h'的头文件
int main(){ // 每一行两个斜线后面的内容表示【注释】,注释并不会被识别为代码内容
  printf("Hello world!\n"); 
  getchar(); // 在一些设备上如果不加上这一行代码,可能会出现类似于闪退的情况
  return 0;
}

我們一行一行開始講。前兩行是功能為引用頭文件編譯預處理指令,其中#include表示引用<>內是被引用的頭文件名字。前兩行分別引用了名字叫做stdio.hstdlib.h的頭文件

  • 引用是什麼意思:在預處理時,編譯器會簡單粗暴地將頭文件內容複製粘貼,替換掉#include語句所在行
    • 什麼是預處理:編譯器在將C語言代碼編譯可執行文件時,分為多個步驟。預處理是其中的一個步驟
    • 雖然被計算機執行的是可執行文件,但我們還是經常說執行代碼
  • 至於這兩個頭文件具體在電腦的哪個位置,不同設備和環境中頭文件的位置可能不同,感興趣可以百度搜索
  • 你可以在大多數代碼編輯器中長按ctrl,然後用鼠標點擊代碼中的頭文件名字,就可以查看該頭文件的內容
  • 引用標準庫的頭文件的時候,需要用<>把頭文件名字框起來。如果引用程式設計師自己寫的頭文件,則使用""
  • 很多初學者會把stdio當做是studio。實際上,stdstandard的縮寫,i表示inputo表示output。該頭文件的功能是給出標準輸入輸出函數聲明(這裏涉及到連結)。stdlib中的liblibrary,這裏並不會是圖書館,而是的意思

然後是3-7行。這裏實現了一個名字叫main()函數int的意思是整形(一種數據類型,簡稱類型),寫在函數名字前面表示這個函數的返回值的類型是int類型

  • intinteger的縮寫,這個數據類型通常用來表示和存儲整數
  • 通常不用整形這個詞來稱呼int類型,更常用的稱呼是int類型

main()函數是一個特殊的函數,被稱為入口函數。之所以main()函數被叫做入口函數,是因為編譯後的可執行文件會從調用main()函數開始

  • 一些教程會將第三行寫成int main(void),這個具體在函數聲明時會講到,不推薦這樣寫
  • 有些教程會寫成int main(int argc, char **argv)(第二個形式參數(或簡稱形參char **argv也可以寫成char argv[],這兩種寫法都是正確的,這裏涉及到命令行參數的問題,後面的內容會詳細講到
  • 還有一種寫法是main(),這種寫法省略了前面的int。早期的C語言是可以省略int的,但現在不推薦這樣寫
  • 一些教程會將左大括號{寫到main()函數的下面,這種寫法是正確的。這種寫法涉及到代碼風格問題,下面會詳細講到

main()函數後面由左大括號和右大括號框柱的內容叫代碼塊,代碼塊中有多條語句,每一條語句後面都要加一個分號。如果調用了main() 函數,代碼塊中的語句會從上到下從左到右逐條執行

可以看到,main( )代碼塊內部有縮進,這個會在後面代碼風格部分講到

  • 通常縮進是一個tab,或兩個或4個空格
#include <stdio.h> 
#include <stdlib.h> 
int main(){
  printf("Hello world!\n"); 
  getchar(); 
  return 0;
}

現在簡單介紹函數調用。第四行調用了一個叫做printf()的函數,該函數可用於打印字符串

  • 計算機科學的函數與數學的函數有很大差別,請注意
  • 這裏的打印並不是在打印機上打印,而是在光標處打印。每次打印一個字符時,光標位置右移,同時原來光標位置替換為被打印的字符,詳情可以看下面的視頻
  • 一些教程,如C語言中文網,會用puts()實現打印字符串功能。它與printf()函數有區別,但在此代碼中可以互相替換
  • printf()中的f是format的縮寫,意為按照指定格式打印
寬屏模式顯示視頻

函數(function)調用的語法是function_name(argument1, argument2, ...),函數名字後面需要跟着一個括號,括號中按順序填充函數參數(argument)。如果函數沒有參數,可以將括號內部置空,但不允許省略括號

  printf("Hello world!\n"); 
  getchar(); 

上面兩行分別調用了兩個函數,第一個是printf()。這裏它只有一個參數,為字符串常量"Hello world\n"。第二個函數getchar()沒有參數

  • 一些教程會把getchar()換成system("pause")。這是為了讓程序阻塞在這裏,以防止程序窗口一執行完printf()語句就自動關閉,就像閃退一樣,這樣就會看不到打印效果。後面的代碼示例會省略getchar()
    • getchar()函數的本來用途是,從'標準輸入流'stdin中讀取一個字符並返回。它會等待用戶通過鍵盤鍵入字符,待用戶輸入回車後,再從緩衝區中讀取字符
    • system()函數的本來用途是,調用系統命令shell命令,其唯一的一個字符串常量參數是將要被調用的命令
      • pause系統命令會等待用戶在鍵盤鍵入任意鍵
        • 關於任意鍵(any key):曾有一個公司推出的軟件中其中一個部分有這樣的提示語,Press Any Key to Continue這時軟件用戶只需要在鍵盤上鍵入任意鍵即可繼續。然後很多用戶打電話詢問公司Any Key在鍵盤的哪個位置。原來很多用戶認為這裏需要按下一個鍵盤上一個名字叫做Any Key的鍵,而用戶在鍵盤上找不到這個鍵。所以後來很多提示語都變成了Press Enter to Continue,而實際上用戶鍵入任何鍵都可以
      • shell這個詞據我所知並沒有合適的中文翻譯,一些人會戲稱地翻譯為貝殼終端terminal的翻譯,而不是shell的翻譯。常見的shell有bashpowershell

代碼的倒數第二行return 0;的意思是,main()函數的返回值0,或者說,main()函數返回0

  • main()函數的返回值具有特殊的意義,它表示程序的返回值。通常如果一個程序的返回值為0,表示程序正常運行,用其他值(比如-1)表示程序異常終止
    • bash中,可以通過echo (?$)來查看最後一個運行的程序的返回值

關於註釋:C語言的註釋有兩種,分別是//这种注释可以注释掉//后面的内容/* 这种注释可以跨行 */兩種。註釋內容不會被識別為代碼內容,也不會被執行,可用於便於未來的程式設計師包括未來的自己閱讀代碼

  • 註釋掉是什麼意思:有時為了代碼調試(debug),故意在代碼某一行前面加上//,以將該行代碼內容變為註釋,這樣被註釋掉的代碼不會被執行,以對比代碼執行結果較未被註釋掉前的區別,以達到調試的效果
#include <stdio.h>
#include <stdlib.h>
int main(){ // 我是一条注释
  printf("1\n"); /* printf()函数
  printf("2\n"); * 可以通过这种方式
  printf("3\n"); * 打印数字
  printf("4\n"); */
  printf("5\n");
  printf("6\n");
  return 0;
}

下面是上面代碼的執行結果,請注意數字2 3 4不會被打印。至於為什麼不會被打印,留作習題

  • 提示:在代碼高亮渲染下,註釋的顏色是和代碼的顏色不一樣的
1
5
6

代碼規範與風格

不同的代碼規範與代碼風格略有區別,但是有一點是確定的,如果要參與別人的代碼項目,必須按照對方的代碼風格貢獻代碼,除非對方的代碼風格本就特別混亂。很多大廠會指定程式設計師的代碼風格,以代碼風格統一滿足強迫症

這一節的所有示例代碼的目的都是為了講解代碼風格,所以不要把注意點放在這些代碼的實際功能和執行效果上。這裏的部分示例代碼是C++代碼。下面部分內容可能涉及到沒學到的內容,暫時不要求理解

  • 說到代碼強迫症,這一點是很多程式設計師都會有的,就是看到特別愚蠢的代碼會忍不住自己上手改一下的感覺,包括尤其是自己的代碼。新人可能會覺得一些規範沒必要,但是如果編程經驗時間長了,就會發現一些寫起來很愚蠢的代碼很難閱讀和維護,看起來很彆扭。我認為也許藝術就是這樣,因為世界上醜陋的東西見多了,便更加認識到美好的東西的美好之處,更加追求美好的東西,這也是為什麼很多人實際上欣賞不來很高深的藝術的原因
  • 代碼也可以看做是一種藝術創作。畫家用畫筆創作,程式設計師用鍵盤創作

事實上,編譯器在編譯的時候,會忽略掉換行空格與tab等空白字符。所以你寫成這樣也可以,儘管這樣會很不易讀

#include <stdio.h>
#include <stdlib.h>
int main(){printf("Hello world!\n");  getchar(); return 0;}

教程最開始的hello world示例代碼是右派寫法,即把左大括號{放到每一行右面。對應的,下面是左派寫法,即把左大括號放到第二行的開頭。一些代碼規範會要求在一些情況使用左派,一些情況使用右派

  • 我個人最開始學編程的時候習慣左派寫法,但是時間長了發現右派寫法可以在屏幕中用更少的空間裝下更多的內容
#include <stdio.h>
#include <stdlib.h>
int main()
{ // 左派写法
  printf("Hello world!\n");
  getchar();
  return 0;
}

需要注意的是,右大括號,和左派寫法的左大括號,需要單獨佔一行。不推薦下面的寫法

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{ if (argc == 1) // 左大括号要单独占一行
  {  // 推荐这样的左大括号用法,单独占一行
    printf("There is no argument\n");  // 这一行并没有和上一行的左大括号写在同一行
    return 0;
  } printf("The number of arguments is %d\n", argc-1);
  // 不推荐上一行这样写,推荐将printf()函数另起一行
  return 0;
}

一行最好不要寫太長,如果寫太長的話,推薦分行寫。如果if ()語句或for ()語句一行需要寫很長的話,我個人推薦下面的寫法

// 提前声明,我不是不会auto
for ( // 在写 `if ()' 语句和 `for()' 语句时,最好在`if'和`for' 之后敲一个空格,以与函数调用区分开
  vector<int>::iterator iter = v.begin(); // 括号内的内容推荐多一个缩进
  iter != v.end();
  iter++){ // 请注意这里和下面,右小括号和左大括号放在一起时的写法。这里的写法比较不容易区分iter++是否是大括号内的内容
  count++;
  int val = *iter;
  if (
    val >= lower_value && // 表达式太长需要换行时,运算符通常写在当前行末尾,而不是第二行开头
    val <  upper_value && // 为了好看,这里的`upper_value'前面敲了两个空格
    is_prime(val)
  ){ // 请注意这里和上面右小括号和左大括号放在一起时的写法
    sum += val;
  }
  else sum -= val;
}

關於空格的使用:通常在每個二元運算符左右各加一個空格,而一元運算符後面不加空格。通常在每個逗號和分號後面加一個空格

  • 這些是二元運算符,通常左右分別加一個空格(但其實有時為了好看反而不加空格)
    • 賦值類:(=) (+=) (-=) (*=) (/=) (%=)等
    • 運算類:(+) (-) (*) (/) (%) (
    • 位運算類:(<<) (>>) (&) (|) (^)
    • 比較運算類:(<) (>) (<=) (>=)
    • 邏輯運算類:(&&) (||)
  • 這些是一元運算符,通常不在右邊加空格
    • 負號:(-)
    • 自加/自減:(++) (--)
    • 邏輯非: (!)
    • 按位非:(~)
    • 取地址:(&)
    • 指針解引用:(*)
  • 這些運算符不加空格
    • 結構體成員訪問:(.) (->)
    • 函數調用 (f())

有時為了排版好看,故意用多個空格或tab,比如下面代碼

void Bit_vector::write_buffer_to_file(FILE* output){
    fwrite(&capacity,               sizeof(int), 1,                 output);
    fwrite(&end_bit_index_of_byte,  sizeof(int), 1,                 output);
    fwrite(&end_byte_index,         sizeof(int), 1,                 output);
    fwrite(buffer,                  sizeof(char),end_byte_index+1,  output);
}

第一章 類型與編碼

上一章寫起來特別抽象,可能對於沒有編程基礎的人很難理解。問題不大,直接進入第一章。本教程有意將常量運算與變量運算分開,以便於講解更深入的內容

printf()函數入門

先介紹一個簡單的printf()打印功能,這裏用於打印int類型的數據。printf()函數的詳情功能可以查看這個頁面,下面也會詳細介紹

  • "%d\n"中的%d佔位符
    • printf()函數會掃描第一個參數的字符串掃描並打印,如果掃描到佔位符,則用printf()後面的參數代替佔位符
    • 尤其注意下面第二個printf()函數第一個參數有兩個佔位符。打印時,第一個佔位符被替換成了第二個參數9,第二個佔位符被替換成了第三個參數30
    • 打印不同類型時需要用到不同的佔位符,比如%d用於打印整形,%c用於打印char類型等,詳情查看這個頁面
  • "%d\n"中的'\n'表示換行,即把光標移動到下一行的開始位置
    • C語言中,單個字符字面量用單引號括起來,而字符串字面量用雙引號(關於字面量,將在後面詳細介紹)
    • 如果不加上換行的話,輸出結果會黏在一起,就像這樣2023930192359
#include <stdio.h>
#include <stdlib.h>
int main(){
  printf("%d\n", 2023); 
  printf("%d\n%d\n", 9, 30);
  printf("%d\n", 9+10);
  printf("%d\n", 3+10*2); // (*)的优先级大于(+)
  printf("%d\n", 10-3-2); // (-)是左结合的
  printf("%d\n", 10-(3-2)); // (())可以改变运算的优先级
  return 0;
}

上面代碼的執行結果是下面

2023
9
30
19
23
5
9

運算順序、優先級與結合性

下面先介紹運算符的運算順序、優先級與結合性。關於運算符具體的功能,將在後面的內容分節介紹

上面代碼中,printf("%d\n", 3+10*2);的結果是打印23。注意這裏的運算順序,並不是先運算(3+10)*2,而是先運算3+(10*2)。我們可能認為這理所當然,而在C語言中,這涉及到一個優先級的問題。在這裏,*的優先級是高於+

優先級和結合性可以查看, 這個網站。括號可以改變運算的優先級

然後說什麼是結合性。在運算10-3-2的時候,我們理所當然認為應該先運算(10-3)-2,而不是10-(3-2)。因為,-左結合的,或者叫從左到右運算。一些運算符是右結合的,比如(=)。雖然還沒到學變量的時候,但還是給出下面的代碼

#include <stdio.h>
#include <stdlib.h>
int main(){
  int a;
  a = 3+2;// (=)是右结合的,所以会先计算(=)右边的内容,等到结果5计算完毕后,再进行(=)运算
  printf("%d\n", a);
  return 0;
}

優先級相同的運算符的結合性一定相同。運算時,先運算優先級高的,然後運算優先級低的。同等優先級的,按照結合性進行從左到右或者從右到左運算。括號可以改變優先級

更多的進位制與char類型

上面我們涉及到的都是int類型的常量,現在介紹一個新的類型char和它的編碼表示。一個char類型的大小是一個字節(byte),一個字節是8位二進制數字。下面一段介紹什麼是二進制數字

我們經常使用的是十進制數字,十進制數字是由0123456789這些數字之中的一個或多個組成的串(但不可以有前導零)。二進制數字是一串用01組成的串(可以有前導零

  • 為了區分各個進制的數字,通常在數字前面加上前綴。C語言中,十進制數字無需前綴,二進制數字前綴為0b,八進制數字前綴為0,十六進制數字常用前綴為0x
  • 二進制數字可以有前導零的意思就是,類似於0b00001010這樣的8位二進制數字是合法的。十進制數字通常不可以有前導零,所以1023是合法的十進制數字,而01023不是合法的十進制數字

二進制數字和十進制數字可以互相換算,詳情可以查看此文。下面是二進制數字0b01001010換算成十進制數字的例子,其中,每一個二進制位的位置對應着一個權重。運算時,將每一位的乘以這一位對應的權重,然後求和,就可以得到十進制數字

  128 64 32 16  8  4  2  1 每一位的权重
    *  *  *  *  *  *  *  * 乘以
  +--+--+--+--+--+--+--+--+
ob| 0| 1| 0| 0| 1| 0| 1| 0|每一位的值
  +--+--+--+--+--+--+--+--+
    0+64+ 0+ 0+ 8+ 0+ 2+ 0 得到的数字
   = 74                    加在一起就是对应的十进制数字

計算機科學常見數字表示還有十六進制,它由0123456789ABCDEF16個字符組成,其中字母可以小寫。下面給出了每一個字符對應的十進制數字對應表,以及一個十六進制數字換算示例

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| F| E| D| C| B| A| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0| 
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 65536  4096   256    16     1
     *     *     *     *     *    
+-----+-----+-----+-----+-----+
|  0xa|    6|    0|    f|    3|
|   10|    6|    0|   15|    3|
+-----+-----+-----+-----+-----+
655360+24576+    0+  240+    3=680179

上文提到,char類型是一個字節大小,而一個字節是8個二進制位。因為每個二進制位都有兩種可能(分別是01),所以char類型的數字一共有256種,可以用來表示-128~1270~255之間的數字,也可以用來表示字符,畢竟charcharacter的縮寫

char類型的值可以用來表示ASCII碼字符,ASCII碼字符與char類型的值的對應關係可以查看這個網頁。單個ASCII字符的字面量,例如單個'a',的類型也是char類型

  • 什麼是字面量:在代碼中,字面量是與變量區分開的概念。字面量直接表示它的的(value),也就是說,程式設計師可以直接從字面量中看出一個字面量的值。例如我們可以從字面量0xf中看出它的值是int類型的十進制數字15。而對於變量,無法從它的名字看出它的值
    • int類型的字面量沒有後綴,所以沒有後綴的,不帶小數點的數字字面量都是int類型
    • 字面量都是常量

TODO:可以使用兩個十六進制數字來表示一個字節

基本數據類型與數據編碼表示

TODO:大端與小端

TODO:sizeof()關鍵字

位運算

位運算一共有6種,分別是按位非~、按位與&、按位或|、按位異或、左移、右移

我們從最簡單的運算:邏輯非運算講起。我們知道,計算機通過01的編碼來存儲數據。非運算很簡單,即在運算後,把操作數每一個二進制位中的0變成1,把1變成0。邏輯非的真值表以及一個運算示例如下


  • 早期的C語言是沒有 (&&) (||)運算符的,都是用(&) (|)來充當[來源請求],所以(&) (|)的運算優先級是歷史遺留問題


自動類型轉換

類型轉換分為兩種,一種是自動類型轉換,一種是手動類型轉換

手動類型轉換

C語言中手動類型轉換的語法是第一行。C++中支持更複雜的類型轉換,可以使用第二條語法來增強可讀性

(type) value
type(value)

手動類型轉換常用於解決除法運算結果是整數的問題

#include <stdio.h>
#include <stdlib.h>
int main(){
  printf("%lf\n", (double)10 / 3); // 先将10转换为double类型,然后10/3就变成了(double)除以(int)。根据上述自动类型转换内容,除号的两个操作数均变为(double),所以运算结果是3.33333
  printf("%lf\n", 10/(double)3); // 
  printf("%lf\n", (double)(10 / 3));
  return 0;
}

上面代碼的執行結果是下面。為什麼第三行輸出是3.000000,因為它是先進行的(int 10)/(int 10),得到(int 3),然後由(int 3)轉為double類型

3.333333
3.333333
3.000000

函數入門

#include <stdio.h>
#include <stdlib.h>
// 习惯将不同作用的代码用空行分开。比如上面的代码是引用头文件,下面的代码是函数声明
int f(); // 一些教程会在函数被实现之前,在开头给出其声明
// 这里的空行也是如此,用于分开函数声明和函数实现
int f(void){ // 此函数先给出的声明,然后给出的实现
  return 10;
}
// 习惯为了好看,在函数实现后加一个空行
char g(){ // 此函数声明时同时实现
  return '+; //43
}

int main(){
  printf("%d\n", f());
  printf("%c\n", g());
  printf("%d\n", f()+g());
  printf("%c\n", getchar());
  return 0; 
}

上面代碼的執行結果是下面

10
63
53

然後程序會阻塞到這裏。此時輸入一個字符,假設是a,然後回車,然後程序就會將這個字符打印出來,然後退出 上面代碼聲明了三個函數,這三個函數的返回類型第一個和第三個是int類型,第二個是char類型。這兩個函數都沒有參數,沒有參數的函數可以在其參數列表置空,也可以寫入void

  • 函數返回值應該要麼與函數聲明的返回類型一致,要麼可以自動轉換為函數聲明的返回類型

章末知識窗:CPU層面運算是怎樣進行的

  • 章末知識窗內容是選學內容

編譯器將C語言代碼編譯可執行文件,可執行文件內部是可以直接由CPU執行的機器碼。機器碼可讀性很差,所以通常將一些機器碼簡記為匯編。匯編有兩種風格,分別是IntelAT&T,這兩種風格有很大差別。此處使用AT&T匯編風格。linux系統中可以使用objdump命令查看一個可執行文件的反匯編代碼

  • 很多IDE將代碼的編譯等步驟集成了起來,所以你可能只需要點一下按鈕,IDE就會自動幫你完成編譯步驟,然後直接執行已編譯後的程序
48 83 ec 08
sub $0x8, %rsp # 汇编代码的注释使用的是井号,而不是两个斜线

上面的示例中,第一行機器碼(用十六進制數字表示)對應的匯編代碼為第二行

運算時,CPU會將數據存儲到寄存器中,一些CPU內有16個寄存器。CPU的運算單元可以根據寄存器的值以及機器代碼來取出其中兩個寄存器的值,進行運算,並將運算結果存入其中一個寄存器中。在調用函數時,寄存器用於傳遞函數參數

#include <stdio.h>
int main(){
  printf("%d\n", 3+10*2):
  return 0;
}

上面的C語言代碼可以編譯後的可執行文件的反匯編是下面的匯編代碼,可以通過看註釋來體驗一下最底層CPU層面代碼是怎樣運行的。需要注意的是,幾乎所有編譯器都會進行優化,所以你進行反編譯的時候,極有可能不會得到和下面相同的反匯編代碼(所以下面的反匯編是我根據現有代碼改編的) TODO:添加代碼高亮,將註釋對其

<main>:
mov $0xa, %edx         # 将寄存器%edx赋值为立即数$3.汇编中,立即数前面需要加$符号,0x表示该立即数十六进制数字表示。$0xa对应十进制数字是`10'
mov $0x2, %eax         # 将寄存器%eax赋值为立即数2。此时,%edx寄存器的值为10,%eax寄存器的值为2
imul %eax, %edx        # 让寄存器%eax的值乘以%edx的值,结果保存在%edx寄存器中
mov $0x3, %eax         # 将寄存器%eax的值赋值为立即数3。此时,%edx寄存器的值为20,%eax寄存器的值为3
add %edx, %eax         # 让寄存器%edx的值乘以%eax的值,结果保存在%eax寄存器中
mov %eax %esi          # 将寄存器%eax的值赋值给%esi,这样%esi的值就是刚刚的运算结果。%esi用于传递printf()函数的第二个参数
lea 0xe83(%rip), %rax  # % 这一行和下两行可以忽略
mov %rax, %rdi         # 这一行和上一行用于将字符串常量"%d\n"作为第一个参数传入
mov $0x0 %eax 
call <printf@plt>      # 调用printf()函数。下面几行可以忽略
mov $0x0, %eax         # 对应return 0。%eax用于传递函数的返回值
leave
ret # 函数调用结束,返回

章末知識窗:C語言的版本

第三章 變量與條件

前兩章都是關於常量運算的,這裏開始變量運算

變量的聲明與關鍵字

TODO:變量的名字不可以是關鍵字 TODO:變量的關鍵字標記

    • 什麼是常量:程序不可以修改的量
    • 什麼是變量:變量這個詞具有多義性,其概念將會在第三章詳細介紹。
    • C語言中,變量可以是常量,這種變量被稱為常變量const關鍵字可以標記一個變量為常變量,後面會詳細介紹

變量的地址與函數棧

TODO:靜態變量。雖然這裏還沒有講到變量的自加運算和指針,但還是給出一個靜態變量的使用示例 TODO:CSAPP中將program stack譯作程序棧

變量的運算

TODO:需要注意自加與自減運算

邏輯運算與三目運算

TODO:的返回值是0或1

if-else語句

TODO:可以沒有else

TODO:if-else if-else 是一種特殊的 if-else 使用方式

函數的聲明與實現

TODO:是否是計數

函數的調用與函數庫

TODO:math函數庫

switch語句

章末知識窗:匯編層面是怎樣實現跳轉的

TODO:調換下順序

goto語句

TODO:如果goto來goto去可能會太亂,不推薦使用

第章 循環與遞歸

while循環

for循環

do-while循環

遞歸函數

TODO:用計算10000!%10001為例引入群論,順便介紹王義和的離散數學引論 TODO:stack overflow

章末知識窗:其他編程語言是怎樣運行的

我們都知道,C語言是編譯型編程語言,也就是說,C語言代碼被編譯器編譯為可執行文件,然後才可以執行。本質上,被執行的並不是C語言代碼,而是被可執行文件


第四章 指針與數組

一級指針及其運算

高級指針

內存的申請與釋放

TODO:介紹malloc free函數,內存泄漏和堆

數組與指針

多維數組

C語言字符串

函數指針

章末知識窗:關於最早的C語言編譯器

TODO:一個先有雞還是先有蛋的問題


第四章 結構體與面向對象

typedef關鍵字

結構體

TODO:C語言和C++的struct不同 TODO:介紹sizeof()操作,因為結構體內部分配內存情況未知

結構體的訪問與指針訪問

面向對象與句柄

編程範式大概可分為兩種:面向過程和面向對象(object-oriented programming,簡稱OOP或OO看上去很唬人——我在用面向對象編程哎——其實不是什麼新鮮東西

面向結果編程(認真你就輸了

還有一種編程範式是面向結果編程,通常用於糊弄編程作業。比如下面的Python代碼用於糊弄打印前10個質數的作業

  • 因為大多數oj只看程序的運行結果,而不會關心代碼是怎麼寫的
print(2) # Python 用井号开始注释,语句后不需要加分号
print(3) # Python 的 print()函数可以自动换行
print(5) '''Python 也可以用这种方式作为注释'''
print(7) '''这种注释的本质是一个没有被赋值给变量的字符串'''
print(9) '''这种注释可以跨行'''
print(11) # 你发没发现,你现在可以看懂一些Python代码了
print(13) 
print(17)
print(19) 
print(23)
print(29)

TODO:需要在同一個文件夾目錄下 TODO:其中file是一個FILE*類型的文件句柄。這其中,文件句柄並不是文件本身,但是程式設計師可通過文件句柄來讀取文件

#include <stdio.h>
#include <stdlib.h>
int main(){
  const char* file_name = "file.txt"
  FILE* file = fopen("file.txt", "r+");
  if (file == NULL){
    printf("Unable to open %s\n");
    return -1;// 在程序出现错误时,通常不return 0
  }
  for(char c; c = fgetc(); c!=EOF){
    putchar(c);
  }
  printf("\n");
  return 0;
}


第五章 預處理與連結

這一章就要實踐起來了,打開Code::Blocks或者VSCode,以及安裝一個linux虛擬機(推薦用VMVare和Ubuntu入門),因為很多指令在虛擬機環境下更容易操作

紙上得來終覺淺,絕知此事要躬行
——陸游《冬夜讀書示子聿》

熟悉虛擬機的操作

編譯實際上是怎樣進行的

編寫自己的頭文件

編寫Make文件與手動連結

TODO:macro名字的由來

編譯預處理指令

第六章 實現一個鍊表

很多C語言教程會在這部分使用實現一個2048遊戲、十步萬度遊戲、掃雷遊戲或者迷宮遊戲等作為最後一章的實踐內容

簡單的數據結構:鍊表

TODO: 二叉樹的定義和操作,這裏選擇不帶有頭結點的二叉樹,每次執行返回頭結點的指針

實現鍊表的插入

TODO:printf()函數debug

第七章 學完這些學什麼

https://pic4.zhimg.com/v2-163d07a2b7b353ff4925afb4a17100bb_b.jpeg
‌外部圖片
你已經學會數學了

你已經攻略C語言娘了,現在,嘗試一下下面的內容吧

C語言是學習其他編程語言的跳板。如果能學明白C語言,那麼其他編程語言自然不會有太多吃力。因為C語言已經有50多年的歷史,在它之後的語言都或多或少參考了一些C語言

C語言被大量應用在嵌入式。可以嘗試買一個51單片機玩玩,大概80軟妹幣以內,然後學習模電和數電,走上嵌入式工程師的道路。找時間我也可以出一個單片機教程

除了嵌入式以外,C語言的應用場景較少。可以嘗試學一下C++。在高效性上C++的執行效率僅次於C語言。以後有時間我會考慮做一個C++教程

可以直接學CSAPP(Computer System: A Programmers' Perspective,深入理解計算機系統),這個是計算機科學最經典的教材之一,據說一些國外的大學要學好幾遍這本教材[來源請求]

  • 其實我認為這裏的System應該翻譯為體系。這本書的內容並不都是關於計算機系統的
  • 如果沒有C語言基礎,這本書是學不明白的

可以攻略考取大學的計算機專業

可以嘗試參加一些代碼項目,哪怕只能貢獻翻譯也挺好的。可以幫我把這個爛攤子收拾一下U:渚 花/Lua參考手冊(翻譯)

可以看一些開原始碼。如果看不懂的話推薦先學習數據結構與常見算法,和常見設計模式

  • 我認為設計模式可大體按照應用場景分為軟件設計模式遊戲設計模式

可以嘗試學一下軟件工程的內容。學完之後就可以嘗試為一些開源軟件貢獻代碼了

可以嘗試攻略一下洛谷娘

可以嘗試攻略一些常見的編程語言娘,比如Java娘Python娘等。Python的設計和其他很多主流編程語言不同,被稱為Pythonic

Python哲學

在Python命令行輸入import this,可得到下面的結果

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

可以嘗試攻略一下世界上最好的編程語言娘PHP娘


外部註釋與連結

此教程主要參考菜鳥教程、蘇小紅《C語言程序設計 第四版》(也推薦這本)


TODO

待添加的內容

  • 因為早期的設備內存空間小,所以早期的C語言代碼的變量名和函數名都會趨於簡單,有說法是當時的變量名和函數名不可以超過7個字符[來源請求],所以atoi()這種可讀性很差的庫函數名是歷史遺留問題。包括函數聲明和實現分離,也是歷史遺留問題,雖然很多教程還是會這樣教,但我認為現在已經完全無必要


一個static關鍵字的例子

#include <stdio.h>
#include <stdlib.h>
// 如果pcount不是NULL,则在其指向的内存地址中写入该函数执行次数
// 否则仅返回加法返回值
int add(int a, int b, int* pcount){
  static int count = 0; // 静态变量只会在函数第一次被调用时被赋值
  count++;
  if (pcount){ // 等价于 if (pcount != NULL)
    *pcount = count;
  }
  return a+b;
}
int main(){
  int count = 0;
  add(1, 2, NULL);
  printf("%d\n", add(1, 2, &count));
  printf("%d\n", count);
  printf("%d\n", add(3, 4, &count));
  printf("%d\n", count);
  return 0;
}

下面是上面代碼的執行結果

3
2
7
3