13 萬字 C 語言從入門到精通保姆級教學2021 年版 (建議收藏)

2021-06-08 13:00:01


友情提示:先關注收藏,再檢視,13 萬字保姆級 C 語言從入門到精通教學。

文章目錄

計算機常識

  • 什麼是計算機 ?

    • 顧名思義,就是能夠進行資料運算的機器(臺式電腦、筆記型電腦、平板電腦、智慧手機)
    • 計算機_百度百科
  • 計算機的發明者是誰 ?

    • 關於電子計算機的發明者是誰這一問題,有好幾種答案:
      • 1936年***英國數學家圖靈***首先提出了一種以程式和輸入資料相互作用產生輸出的計算機***構想***,後人將這種機器命名為通用圖靈計算機
      • 1938年***克蘭德·楚澤***發明了首臺採用***繼電器***進行工作的計算機,這臺計算機命名為***Z1***,但是繼電器是機械式的,並不是完全的電子器材
      • 1942年***阿坦那索夫和貝利***發明了首臺採用***真空管***的計算機,這臺計算機命名為***ABC***
      • 1946年ENIAC誕生,它擁有了今天計算機的主要結構和功能,是通用計算機
  • 現在世界上***公認***的第一臺現代電子計算機是1946年在美國賓夕法尼亞大學誕生的ENIAC(Electronic Numerical Integrator And Calculator)
  • 計算機特點是什麼 ?
    • 計算機是一種電器, 所以計算機只能識別兩種狀態, 一種是通電一種是斷電

    • 正是因為如此, 最初ENIAC的程式是由很多開關和連線電線來完成的。但是這樣導致***改動一次程式要花很長時間***(需要人工重新設定很多開關的狀態和連線線)

    • 為了提高效率,工程師們想能不能把程式和資料都放在記憶體中, 數學家馮·諾依曼將這個思想以數學語言系統闡述,提出了儲存程式計算機模型(這是所謂的馮·諾依曼機)

    • 那利用數學語言如何表示計算機能夠識別的通電和斷電兩種狀態呢?

      • 非常簡單用0和1表示即可
      • 所以計算機能識別的所有指令都是由0和1組成的
      • 所以計算機中儲存和操作的資料也都是由0和1組成的

0和1更準確的是應該是高電平和低電平, 但是這個不用瞭解, 只需要知道計算機只能識別0和1以及儲存的資料都是由0和1組成的即可。


什麼是計算機程式 ?

  • 計算機程式是為了告訴計算機"做某件事或解決某個問題"而用"***計算機語言***編寫的命令集合(語句)

  • 只要讓計算機執行這個程式,計算機就會自動地、有條不紊地進行工作,計算機的一切操作都是由程式控制的,離開程式,計算機將一事無成

  • 現實生活中你如何告訴別人如何做某件事或者解決某個問題?

    • 通過人能聽懂的語言: 張三你去樓下幫我買一包煙, 然後順便到快遞箱把我的快遞也帶上來
    • 其實我們通過人能聽懂的語言告訴別人做某件事就是在傳送一條條的指令
    • 計算機中也一樣, 我們可以通過計算機語言告訴計算機我們想做什麼, 每做一件事情就是一條指令, 一條或多條指令的集合我們就稱之為一個計算機程式

什麼是計算機語言 ?

  • 在日常生活、工作中, 語言是人們交流的工具
    • 中國人和中國人交流,使用中文語言
    • 美國人和美國人交流,使用英文語言
    • 人想要和計算機交流,使用計算機語言
  • 可以看出在日常生活、工作中,人們使用的語言種類很多
    • 如果一個很牛人可能同時掌握了中文語言和英文語言, 那麼想要和這個人交流既可以使用中文語言,也可以使用英文語言
    • 計算機其實就是一個很牛的人, 計算機同時掌握了幾十門甚至上百門語言, 所以我們只要使用任何一種計算機已經掌握的語言就可以和計算機交流

常見的計算機語言型別有哪些 ?

  • 機器語言
    • 所有的程式碼裡面只有0和1, 0表示不加電,1表示加電(紙帶儲存時 1有孔,0沒孔)
    • 優點:直接對硬體產生作用,程式的執行效率非常非常高
    • 缺點:指令又多又難記、可讀性差、無可移植性
  • 組合語言
    • 符號化的機器語言,用一個符號(英文單詞、數位)來代表一條機器指令
    • 優點:直接對硬體產生作用,程式的執行效率非常高、可讀性稍好
    • 缺點:符號非常多和難記、無可移植性
  • 高階語言
    • 非常接近自然語言的高階語言,語法和結構類似於普通英文
    • 優點:簡單、易用、易於理解、遠離對硬體的直接操作、有可移植性
    • 缺點:有些高階語言寫出的程式執行效率並不高
  • 對比(利用3種型別語言編寫1+1)
    • 機器語言
      • 10111000 00000001 00000000 00000101 00000001 00000000
    • 組合語言
      • MOV AX, 1 ADD AX, 1
    • 高階語言
      • 1 + 1

什麼是C語言?

  • C語言是一種用於和計算機交流的高階語言, 它既具有高階語言的特點,又具有組合語言的特點
    • 非常接近自然語言
    • 程式的執行效率非常高
  • C語言是所有程式語言中的經典,很多高階語言都是從C語言中衍生出來的,
    • 例如:C++、C#、Object-C、Java、Go等等
  • C語言是所有程式語言中的經典,很多著名的系統軟體也是C語言編寫的
    • 幾乎所有的作業系統都是用C語言編寫的
    • 幾乎所有的計算機底層軟體都是用C語言編寫的
    • 幾乎所有的編輯器都是C語言編寫的

C語言歷史

  • 最早的高階語言:FORTRAN–>ALGOL–>CPL–>BCPL–>C–>C++等

「初,世間無語言,僅電路與連線。及大牛出,天地開,始有 FORTRAN、 LISP、ALGOL 隨之, 乃有萬種語」

  • 1963年英國劍橋大學推出了CPL(Combined Programming Langurage)語言。 CPL語言在ALGOL 60的基礎上接近硬體一些,但規模比較大,難以實現
  • 1967年英國劍橋大學的 Matin Richards(理查茲)對CPL語言做了簡化,推出了 BCPL (Base Combined Programming Langurage)語言
  • 1970年美國貝爾實驗室的 Ken Thompson(肯·湯普遜) 以 BCPL 語言為基礎,又作了進一步的簡化,設計出了很簡單的而且很接近硬體的 B 語言(取BCPL的第一個字母),並用B語言寫出了第一個 UNIX 作業系統。但B語言過於簡單,功能有限
  • 1972年至1973年間,貝爾實驗室的 Dennis.Ritchie(丹尼斯·裡奇) 在 B語言的基礎上設計出了C語言(取BCPL的第二個字母)。C語言即保持 BCPL 語言和B語言的優點(精練、接近硬體),又克服了他們的缺點(過於簡單,資料無型別等)

C語言標準

  • 1983年美國國家標準局(American National Standards Institute,簡稱ANSI)成立了一個委員會,開始制定C語言標準的工作
  • 1989年C語言標準被批准,這個版本的C語言標準通常被稱為ANSI C(C89)
  • 1999年,國際標準化組織ISO又對C語言標準進行修訂,在基本保留原C語言特徵的基礎上,針對應該的需要,增加了一些功能,命名為***C99***
  • 2011年12月,ANSI採納了ISO/IEC 9899:2011標準。這個標準通常即***C11,它是C程式語言的現行標準***

C語言現狀


為什麼要學習C語言?

  • 40多年經久不衰
  • 瞭解作業系統、編譯原理、資料結構與演演算法等知識的最佳語言
  • 瞭解其它語言底層實現原理必備語言
  • 基礎語法與其它高階語言類似,學會C語言之後再學習其它語言事半功倍,且知根知底

當你想了解底層原理時,你才會發現後悔當初沒有學習C語言
當你想學習一門新的語言時, 你才會發現後悔當初沒有學習C語言
當你使用一些高階框架、甚至系統框架時發現提供的API都是C語言編寫的, 你才發現後悔當初沒有學習C語言
學好數理化,走遍天下都不拍
學好C語言,再多語言都不怕


如何學好C語言

學習本套課程之前學習本套課程中學習本套課程之後
[外連圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-gHyaoC72-1623039894713)(https://upload-images.jianshu.io/upload_images/647982-c724f6cd01191121.png?imageMogr2/auto-orient/strip)]
  • 如何達到這樣的效果

工欲善其事必先利其器

編寫C語言程式用什麼工具 ?

  • 記事本(開發效率低)
  • Vim(初學者入門門檻高)
  • VSCode(不喜歡)
  • eclipse(不喜歡)
  • CLion(深愛, 但收費)
  • Xcode(逼格高, 但得有蘋果電腦)
  • Qt Creator(開源免費,跨平臺安裝和執行)

什麼是Qt Creator ?

  • Qt Creator 是一款新的輕量級整合式開發環境(IDE)。它能夠跨平臺執行,支援的系統包括 Windows、Linux(32 位及 64 位)以及 Mac OS X
  • Qt Creator 的設計目標是使開發人員能夠利用 Qt 這個應用程式框架更加快速及輕易的完成開發任務
  • 開源免費, 簡單易用, 能夠滿足學習需求

整合式開發環境(IDE,Integrated Development Environment )是用於提供程式開發環境的應用程式,一般包括程式碼編輯器編譯器偵錯器和圖形化使用者介面等工具。整合了程式碼編寫功能、分析功能、編譯功能、偵錯功能等一體化的開發軟體服務套。


Qt Creator安裝

  • 切記囫圇吞棗, 不要糾結裡面的東西都是什麼含義, 初學者安裝成功就是一種成功

  • 下載Qt Creator離線安裝包:

    • http://download.qt.io/archive/qt/5.11/5.11.0/
  • 以管理身份執行離線安裝包

  • 下一步,下一步,下一步,等待ing…


    • +
      +
  • 注意安裝路徑中最好不要出現中文

  • 對於初學者而言全選是最簡單的方式(重點!!!)





  • 設定Qt Creator開發環境變數





你的安裝路徑\5.11.0\mingw53_32\bin
你的安裝路徑\Tools\mingw530_32\bin

  • 啟動安裝好的Qt Creator

  • 非全選安裝到此為止, 全選安裝繼續往下看
    • 出現這個錯誤, 忽略這個錯誤即可
  • 等待安裝完畢之後解決剛才的錯誤
    • 找到安裝目錄下的strawberry.msi,雙擊執行




什麼是環境變數?

  • 開啟我們新增環境變數的兩個目錄, 不難發現裡面大部分都是.exe的可執行程式
  • 如果我們不設定環境變數, 那麼每次我們想要使用這些"可執行程式"都必須"先找到這些應用程式對應的資料夾"才能使用
  • 為了方便我們在電腦上"任何地方"都能夠使用這些"可執行程式", 那麼我們就必須新增環境變數, 因為Windows執行某個程式的時候, 會先到"環境變數中Path指定的路徑中"去查詢

為什麼要設定系統變數,不設定使用者變數

  • 使用者變數只針對使用這臺計算機指定使用者
    • 一個計算機可以設定多個使用者, 不同的使用者用不同的使用者名稱和密碼
    • 當給計算機設定了多個使用者的時候,啟動計算機的時候就會讓你選擇哪個使用者登入
  • 系統變數針對使用這臺計算機的所有使用者
    • 也就是說設定了系統變數, 無論哪個使用者登入這臺計算機都可以使用你設定好的工具

Qt Creator快捷鍵

如何建立C語言程式

  • 這個世界上, 幾乎所有程式設計師入門的第一段程式碼都是Hello World.
  • 原因是當年C語言的作者Dennis Ritchie(丹尼斯 裡奇)在他的名著中第一次引入, 傳為後世經典, 其它語言亦爭相效仿, 以示敬意

如何建立C語言檔案





C語言程式組成

  • 手機有很多功能, 「開機」,「關機」,「打電話」,「傳簡訊」,"拍照"等等

  • 手機中的每一個功能就相當於C語言程式中的一個程式段(函數)

  • 眾多功能中總有一個會被先執行,不可能多個功能一起執行

  • 想使用手機必須先執行手機的開機功能

  • 所以C語言程式也一樣,由眾多功能、眾多程式段組成, 眾多C語言程式段中總有一個會被先執行, 這個先執行的程式段我們稱之為"主函數"

  • 一個C語言程式由多個"函數"構成,每個函數有自己的功能

  • 一個程式***有且只有一個主函數***

  • 如果一個程式沒有主函數,則這個程式不具備執行能力

  • 程式執行時系統會***自動呼叫***主函數,而其它函數需要開發者***手動呼叫***

  • 主函數有固定書寫的格式和範寫

函數定義格式

  • 主函數定義的格式:
    • int 代表函數執行之後會返回一個整數型別的值
    • main 代表這個函數的名字叫做main
    • () 代表這是一個函數
    • {} 代表這個程式段的範圍
    • return 0; 代表函數執行完之後返回整數0
int main() {
    // insert code here...
    return 0;
}
  • 其它函數定義的格式
    • int 代表函數執行之後會返回一個整數型別的值
    • call 代表這個函數的名字叫做call
    • () 代表這是一個函數
    • {} 代表這個程式段的範圍
    • return 0; 代表函數執行完之後返回整數0
int call() {
    return 0;
}

如何執行定義好的函數

  • 主函數(main)會由系統自動呼叫, 但其它函數不會, 所以想要執行其它函數就必須在main函數中手動呼叫
    • call 代表找到名稱叫做call的某個東西
    • () 代表要找到的名稱叫call的某個東西是一個函數
    • ; 代表呼叫函數的語句已經編寫完成
    • 所以call();代表找到call函數, 並執行call函數
int main() {
    call();
    return 0;
}
  • 如何往螢幕上輸出內容
    • 輸出內容是一個比較複雜的操作, 所以系統提前定義好了一個專門用於輸出內容的函數叫做printf函數,我們只需要執行系統定義好的printf函數就可以往螢幕上輸出內容
    • 但凡需要執行一個函數, 都是通過函數名稱+圓括號的形式來執行
    • 如下程式碼的含義是: 當程式執行時系統會自動執行main函數, 在系統自動執行main函數時我們手動執行了call函數和printf函數
    • 經過對程式碼的觀察, 我們發現兩個問題
      • 並沒有告訴printf函數,我們要往螢幕上輸出什麼內容
      • 找不到printf函數的實現程式碼
int call(){
    return 0;
}

int main(){
    call();
    printf();
    return 0;
}
  • 如何告訴printf函數要輸出的內容
    • 將要輸出的內容編寫到printf函數後面的圓括號中即可
    • 注意: 圓括號中編寫的內容必須用雙引號引起來
printf("hello world\n");
  • 如何找到printf函數的實現程式碼
    • 由於printf函數是系統實現的函數, 所以想要使用printf函數必須在使用之前告訴系統去哪裡可以找到printf函數的實現程式碼
    • #include <stdio.h> 就是告訴系統可以去stdio這個檔案中查詢printf函數的宣告和實現
#include <stdio.h>

int call(){
    return 0;
}

int main(){
    call();
    printf("hello world\n");
    return 0;
}

如何執行編寫好的程式

  • 方式1:
    • 點選小榔頭將"原始碼"編譯成"可執行檔案"
    • 找到編譯後的原始碼, 開啟終端(CMD)執行可執行檔案




  • 方式2
    • 直接點選Qt開發工具執行按鈕


main函數注意點及其它寫法

  • C語言中,每條完整的語句後面都必須以分號結尾
int main(){
    printf("hello world\n") // 如果沒有分號編譯時會報錯
    return 0;
}
int main(){
    // 如果沒有分號,多條語句合併到一行時, 系統不知道從什麼地方到什麼地方是一條完整語句
    printf("hello world\n") return 0;
}
  • C語言中除了註釋和雙引號引起來的地方以外都不能出現中文
int main(){
    printf("hello world\n"); // 這裡的分號如果是中文的分號就會報錯
    return 0;
}
  • 一個C語言程式只能有一個main函數
int main(){
    return 0;
}
int main(){ // 編譯時會報錯, 重複定義
    return 0;
}
  • 一個C語言程式不能沒有main函數
int call(){ // 編譯時報錯, 因為只有call函數, 沒有main函數
    return 0;
}
int mian(){ // 編譯時報錯, 因為main函數的名稱寫錯了,還是相當於沒有main函數
    return 0;
}
  • main函數前面的int可以不寫或者換成void
#include <stdio.h>
main(){ // 不會報錯
    printf("hello world\n");
    return 0;
}
#include <stdio.h>
void main(){  // 不會報錯
    printf("hello world\n");
    return 0;
}
  • main函數中的return 0可以不寫
int main(){ // 不會報錯
    printf("hello world\n");
}
  • 多種寫法不報錯的原因
    • C語言最早的時候只是一種規範和標準(例如C89, C11等)
    • 標準的推行需要各大廠商的支援和實施
    • 而在支援的實施的時候由於各大廠商利益、理解等問題,導致了實施的標準不同,發生了變化
      • Turbo C
      • Visual C(VC)
      • GNU C(GCC)
    • 所以大家才會看到不同的書上書寫的格式有所不同, 有的返回int,有的返回void,有的甚至沒有返回值
    • 所以大家只需要記住最標準的寫法即可, no zuo no die
#include <stdio.h>
int main(){
    printf("hello world\n");
    return 0;
}

Tips:
語法錯誤:編譯器會直接報錯
邏輯錯誤:沒有語法錯誤,只不過執行結果不正確


C語言程式練習

  • 編寫一個C語言程式,用至少2種方式在螢幕上輸出以下內容
   *** ***
  *********
   *******
    ****
     **
  • 普通青年實現
printf(" *** *** \n");
printf("*********\n");
printf(" *******\n");
printf("  ****\n");
printf("   **\n");
  • 2B青年實現
printf(" *** *** \n*********\n *******\n  ****\n   **\n");
  • 文藝青年實現(裝逼的, 先不用理解)
int  i = 0;
while (1) {
    if (i % 2 == 0) {
        printf(" *** *** \n");
        printf("*********\n");
        printf(" *******\n");
        printf("  ****\n");
        printf("   **\n");
    }else
    {
        printf("\n");
        printf("   ** ** \n");
        printf("  *******\n");
        printf("   *****\n");
        printf("    **\n");
    }
    sleep(1);
    i++;
    system("cls");
}

初學者如何避免程式出現BUG

                          _ooOoo_
                         o8888888o
                         88" . "88
                         (| -_- |)
                          O\ = /O
                      ____/`---'\____
                    .   ' \\| |// `.
                     / \\||| : |||// \
                   / _||||| -:- |||||- \
                     | | \\\ - /// | |
                   | \_| ''\---/'' | |
                    \ .-\__ `-` ___/-. /
                 ___`. .' /--.--\ `. . __
              ."" '< `.___\_<|>_/___.' >'"".
             | | : `- \`.;`\ _ /`;.`/ - ` : | |
               \ \ `-. \_ __\ /__ _/ .-` / /
       ======`-.____`-.___\_____/___.-`____.-'======
                          `=---='

       .............................................
              佛祖保佑                   有無BUG
━━━━━━神獸出沒━━━━━━
         ┏┓    ┏┓
        ┏┛┻━━━━━━┛┻┓
        ┃        ┃
        ┃   ━    ┃
        ┃ ┳┛   ┗┳ ┃
        ┃        ┃
        ┃   ┻    ┃
        ┃          ┃
        ┗━┓    ┏━┛Code is far away from bug with the animal protecting
          ┃    ┃    神獸保佑,程式碼無bug
          ┃    ┃
          ┃    ┗━━━┓
          ┃        ┣┓
          ┃     ┏━━┛┛
          ┗┓┓┏━┳┓┏┛
           ┃┫┫ ┃┫┫
           ┗┻┛ ┗┻┛

      ━━━━━━感覺萌萌噠━━━━━━
        ´´´´´´´´██´´´´´´´
        ´´´´´´´████´´´´´´
        ´´´´´████████´´´´
        ´´`´███▒▒▒▒███´´´´´
        ´´´███▒●▒▒●▒██´´´
        ´´´███▒▒▒▒▒▒██´´´´´
        ´´´███▒▒▒▒██´                      專案:第一個C語言程式
        ´´██████▒▒███´´´´´                 語言: C語言
        ´██████▒▒▒▒███´´                   編輯器: Qt Creator 
        ██████▒▒▒▒▒▒███´´´´                版本控制:git-github
        ´´▓▓▓▓▓▓▓▓▓▓▓▓▓▒´´                 程式碼風格:江哥style
        ´´▒▒▒▒▓▓▓▓▓▓▓▓▓▒´´´´´              
        ´.▒▒▒´´▓▓▓▓▓▓▓▓▒´´´´´              
        ´.▒▒´´´´▓▓▓▓▓▓▓▒                   
        ..▒▒.´´´´▓▓▓▓▓▓▓▒                   
        ´▒▒▒▒▒▒▒▒▒▒▒▒                      
        ´´´´´´´´´███████´´´´´              
        ´´´´´´´´████████´´´´´´´
        ´´´´´´´█████████´´´´´´
        ´´´´´´██████████´´´´             大部分人都在關注你飛的高不高,卻沒人在乎你飛的累不累,這就是現實!
        ´´´´´´██████████´´´                     我從不相信夢想,我,只,相,信,自,己!
        ´´´´´´´█████████´´
        ´´´´´´´█████████´´´
        ´´´´´´´´████████´´´´´
        ________▒▒▒▒▒
        _________▒▒▒▒
        _________▒▒▒▒
        ________▒▒_▒▒
        _______▒▒__▒▒
        _____ ▒▒___▒▒
        _____▒▒___▒▒
        ____▒▒____▒▒
        ___▒▒_____▒▒
        ███____ ▒▒
        ████____███
        █ _███_ _█_███
——————————————————————————女神保佑,程式碼無bug——————————————————————

多語言對比

  • C語言
#include<stdio.h>
int main() {
    printf("南哥帶你裝B帶你飛");
    return 0;
}
  • C++語言
#include<iostream>
using namespace std;
int main() {
    cout << "南哥帶你裝B帶你飛" << endl;
    return 0;
}
  • OC語言
#import <Foundation/Foundation.h>
int main() {
    NSLog(@"南哥帶你裝B帶你飛");
    return 0;
}
  • Java語言
class Test
{
    public static viod main()
    {
        system.out.println("南哥帶你裝B帶你飛");
    }
}
  • Go語言
package main
import  "fmt" //引入fmt庫
func main() {
    fmt.Println("南哥帶你裝B帶你飛")
}

什麼是註釋?

  • 註釋是在所有計算機語言中都非常重要的一個概念,從字面上看,就是註解、解釋的意思
  • 註釋可以用來解釋某一段程式或者某一行程式碼是什麼意思,方便程式設計師之間的交流溝通
  • 註釋可以是任何文字,也就是說可以寫中文
  • 被註釋的內容在開發工具中會有特殊的顏色

為什麼要使用註釋?

  • 沒有編寫任何註釋的程式
void printMap(char map[6][7] , int row, int col);
int main(int argc, const char * argv[])
{
    char map[6][7] = {
        {'#', '#', '#', '#', '#', '#', '#'},
        {'#', ' ', ' ', ' ', '#' ,' ', ' '},
        {'#', 'R', ' ', '#', '#', ' ', '#'},
        {'#', ' ', ' ', ' ', '#', ' ', '#'},
        {'#', '#', ' ', ' ', ' ', ' ', '#'},
        {'#', '#', '#', '#', '#', '#', '#'}
    };
    int row = sizeof(map)/sizeof(map[0]);
    int col = sizeof(map[0])/ sizeof(map[0][0]);
    printMap(map, row, col);
    int pRow = 2;
    int pCol = 1;
    int endRow = 1;
    int endCol = 6;
    while ('R' != map[endRow][endCol]) {
        printf("親, 請輸入相應的操作\n");
        printf("w(向上走) s(向下走) a(向左走) d(向右走)\n");
        char run;
        run = getchar();
        switch (run) {
            case 's':
                if ('#' != map[pRow + 1][pCol]) {
                    map[pRow][pCol] = ' ';
                    pRow++;//3
                    map[pRow][pCol] = 'R';
                }
                break;
            case 'w':
                if ('#' != map[pRow - 1][pCol]) {
                    map[pRow][pCol] = ' ';
                    pRow--;
                    map[pRow][pCol] = 'R';
                }
                break;
            case 'a':
                if ('#' != map[pRow][pCol - 1]) {
                    map[pRow][pCol] = ' ';
                    pCol--;
                    map[pRow][pCol] = 'R';
                }
                break;
            case 'd':
                if ('#' != map[pRow][pCol + 1]) {
                    map[pRow][pCol] = ' ';
                    pCol++;
                    map[pRow][pCol] = 'R';
                }
                break;
        }
        printMap(map, row, col);
    }
    printf("你太牛X了\n");
    printf("想挑戰自己,請購買完整版本\n");
    return 0;
}
void printMap(char map[6][7] , int row, int col)
{
    system("cls");
    for (int i = 0; i < row; i++) {
        for (int j = 0; j < col; j++) {
            printf("%c", map[i][j]);
        }
        printf("\n");
    }
}

  • 編寫了註釋的程式
/*
     R代表一個人
     #代表一堵牆
//   0123456
     ####### // 0
     #   #   // 1
     #R ## # // 2
     #   # # // 3
     ##    # // 4
     ####### // 5

     分析:
     >1.儲存地圖(二維陣列)
     >2.輸出地圖
     >3.操作R前進(控制小人行走)
      3.1.接收使用者輸入(scanf/getchar)
      w(向上走) s(向下走) a(向左走) d(向右走)
      3.2.判斷使用者的輸入,控制小人行走
         3.2.1.替換二維陣列中儲存的資料
             (
                1.判斷是否可以修改(如果不是#就可以修改)
                2.修改現有位置為空白
                3.修改下一步為R
             )
      3.3.輸出修改後的二維陣列
     4.判斷使用者是否走出出口
*/
// 宣告列印地圖方法
void printMap(char map[6][7] , int row, int col);
int main(int argc, const char * argv[])
{
    // 1.定義二維陣列儲存迷宮地圖
    char map[6][7] = {
        {'#', '#', '#', '#', '#', '#', '#'},
        {'#', ' ', ' ', ' ', '#' ,' ', ' '},
        {'#', 'R', ' ', '#', '#', ' ', '#'},
        {'#', ' ', ' ', ' ', '#', ' ', '#'},
        {'#', '#', ' ', ' ', ' ', ' ', '#'},
        {'#', '#', '#', '#', '#', '#', '#'}
    };
    // 2.計算地圖行數和列數
    int row = sizeof(map)/sizeof(map[0]);
    int col = sizeof(map[0])/ sizeof(map[0][0]);
    // 3.輸出地圖
    printMap(map, row, col);
    // 4.定義變數記錄人物位置
    int pRow = 2;
    int pCol = 1;
    // 5.定義變數記錄出口的位置
    int endRow = 1;
    int endCol = 6;
    // 6.控制人物行走
    while ('R' != map[endRow][endCol]) {
        // 6.1提示使用者如何控制人物行走
        printf("親, 請輸入相應的操作\n");
        printf("w(向上走) s(向下走) a(向左走) d(向右走)\n");
        char run;
        run = getchar();
        // 6.2根據使用者輸入控制人物行走
        switch (run) {
            case 's':
                if ('#' != map[pRow + 1][pCol]) {
                    map[pRow][pCol] = ' ';
                    pRow++;//3
                    map[pRow][pCol] = 'R';
                }
                break;
            case 'w':
                if ('#' != map[pRow - 1][pCol]) {
                    map[pRow][pCol] = ' ';
                    pRow--;
                    map[pRow][pCol] = 'R';
                }
                break;
            case 'a':
                if ('#' != map[pRow][pCol - 1]) {
                    map[pRow][pCol] = ' ';
                    pCol--;
                    map[pRow][pCol] = 'R';
                }
                break;
            case 'd':
                if ('#' != map[pRow][pCol + 1]) {
                    map[pRow][pCol] = ' ';
                    pCol++;
                    map[pRow][pCol] = 'R';
                }
                break;
        }
        // 6.3重新輸出行走之後的地圖
        printMap(map, row, col);
    }
    printf("你太牛X了\n");
    printf("想挑戰自己,請購買完整版本\n");
    return 0;
}

/**
 * @brief printMap
 * @param map 需要列印的二維陣列
 * @param row 二維陣列的行數
 * @param col 二維陣列的列數
 */
void printMap(char map[6][7] , int row, int col)
{
    // 為了保證視窗的乾淨整潔, 每次列印都先清空上一次的列印
    system("cls");
    for (int i = 0; i < row; i++) {
        for (int j = 0; j < col; j++) {
            printf("%c", map[i][j]);
        }
        printf("\n");
    }
}

註釋的分類

  • 單行註釋

    • // 被註釋內容
    • 使用範圍:任何地方都可以寫註釋:函數外面、裡面,每一條語句後面
    • 作用範圍: 從第二個斜線到這一行末尾
    • 快捷鍵:Ctrl+/
  • 多行註釋

    • /* 被註釋內容 */
    • 使用範圍:任何地方都可以寫註釋:函數外面、裡面,每一條語句後面
    • 作用範圍: 從第一個/*到最近的一個*/

註釋的注意點

  • 單行註釋可以巢狀單行註釋、多行註釋
// 南哥 // it666.com
// /* 江哥 */
// 帥哥
  • 多行註釋可以巢狀單行註釋
/*
// 作者:LNJ
// 描述:第一個C語言程式作用:這是一個主函數,C程式的入口點
 */
  • 多行註釋***不能***巢狀多行註釋
/* 
哈哈哈
     /*嘻嘻嘻*/
 呵呵呵 
*/

註釋的應用場景

  • 思路分析
/*
     R代表一個人
     #代表一堵牆
//   0123456
     ####### // 0
     #   #   // 1
     #R ## # // 2
     #   # # // 3
     ##    # // 4
     ####### // 5

     分析:
     >1.儲存地圖(二維陣列)
     >2.輸出地圖
     >3.操作R前進(控制小人行走)
      3.1.接收使用者輸入(scanf/getchar)
      w(向上走) s(向下走) a(向左走) d(向右走)
      3.2.判斷使用者的輸入,控制小人行走
         3.2.1.替換二維陣列中儲存的資料
             (
                1.判斷是否可以修改(如果不是#就可以修改)
                2.修改現有位置為空白
                3.修改下一步為R
             )
      3.3.輸出修改後的二維陣列
     4.判斷使用者是否走出出口
*/
  • 對變數進行說明
// 2.計算地圖行數和列數
int row = sizeof(map)/sizeof(map[0]);
int col = sizeof(map[0])/ sizeof(map[0][0]);
  • 對函數進行說明
/**
 * @brief printMap
 * @param map 需要列印的二維陣列
 * @param row 二維陣列的行數
 * @param col 二維陣列的列數
 */
void printMap(char map[6][7] , int row, int col)
{
    system("cls");
    for (int i = 0; i < row; i++) {
        for (int j = 0; j < col; j++) {
            printf("%c", map[i][j]);
        }
        printf("\n");
    }
}
  • 多實現邏輯排序
    // 1.定義二維陣列儲存迷宮地圖
    char map[6][7] = {
        {'#', '#', '#', '#', '#', '#', '#'},
        {'#', ' ', ' ', ' ', '#' ,' ', ' '},
        {'#', 'R', ' ', '#', '#', ' ', '#'},
        {'#', ' ', ' ', ' ', '#', ' ', '#'},
        {'#', '#', ' ', ' ', ' ', ' ', '#'},
        {'#', '#', '#', '#', '#', '#', '#'}
    };
    // 2.計算地圖行數和列數
    int row = sizeof(map)/sizeof(map[0]);
    int col = sizeof(map[0])/ sizeof(map[0][0]);
    // 3.輸出地圖
    printMap(map, row, col);
    // 4.定義變數記錄人物位置
    int pRow = 2;
    int pCol = 1;
    // 5.定義變數記錄出口的位置
    int endRow = 1;
    int endCol = 6;
    // 6.控制人物行走
    while ('R' != map[endRow][endCol]) {
        ... ...
    }

使用註釋的好處

  • 註釋是一個程式設計師必須要具備的良好習慣
  • 幫助開發人員整理實現思路
  • 解釋說明程式, 提高程式的可讀性
    • 初學者編寫程式可以養成習慣:先寫註釋再寫程式碼
    • 將自己的思想通過註釋先整理出來,在用程式碼去體現
    • 因為程式碼僅僅是思想的一種體現形式而已

什麼是關鍵字?

  • 關鍵字,也叫作保留字。是指一些被C語言賦予了特殊含義的單詞
  • 關鍵字特徵:
    • 全部都是小寫
    • 在開發工具中會顯示特殊顏色
  • 關鍵字注意點:
    • 因為關鍵字在C語言中有特殊的含義, 所以不能用作變數名、函數名等
  • C語言中一共有32個關鍵字
12345678
charshortintlongfloatdoubleifelse
returndowhileforswitchcasebreakcontinue
defaultgotosizeofautoregisterstaticexternunsigned
signedtypedefstructenumunionvoidconstvolatile

這些不用專門去記住,用多了就會了。在編譯器裡都是有特殊顏色的。 我們用到時候會一個一個講解這個些關鍵字怎麼用,現在瀏覽下,有個印象就OK了


關鍵字分類

什麼是識別符號?

  • 從字面上理解,就是用來標識某些東西的符號,標識的目的就是為了將這些東西區分開來
  • 其實識別符號的作用就跟人類的名字差不多,為了區分每個人,就在每個人出生的時候起了個名字
  • C語言是由函數構成的,一個C程式中可能會有多個函數,為了區分這些函數,就給每一個函數都起了個名稱, 這個名稱就是識別符號
  • 綜上所述: 程式設計師在程式中給函數、變數等起名字就是識別符號

識別符號命名規則

  • 只能由字母(a~z、 A~Z)、數位、下劃線組成
  • 不能包含除下劃線以外的其它特殊字串
  • 不能以數位開頭
  • 不能是C語言中的關鍵字
  • 識別符號嚴格區分大小寫, test和Test是兩個不同的識別符號

練習

  • 下列哪些是合法的識別符號
fromNo22from#22my_Booleanmy-Boolean2ndObjGUIlnj
Mike2jack江哥_testtest!32haha(da)ttjack_rosejack&rose

識別符號命名規範

  • 見名知意,能夠提高程式碼的可讀性
  • 駝峰命名,能夠提高程式碼的可讀性
    • 駝峰命名法就是當變數名或函數名是由多個單詞連線在一起,構成識別符號時,第一個單詞以小寫字母開始;第二個單詞的首字母大寫.
    • 例如: myFirstName、myLastName這樣的變數名稱看上去就像駝峰一樣此起彼伏

什麼是資料?

  • 生活中無時無刻都在跟資料打交道

    • 例如:人的體重、身高、收入、性別等資料等
  • 在我們使用計算機的過程中,也會接觸到各種各樣的資料

    • 例如: 檔案資料、圖片資料、視訊資料等

資料分類

  • 靜態的資料

    • 靜態資料是指一些永久性的資料,一般儲存在硬碟中。硬碟的儲存空間一般都比較大,現在普通計算機的硬碟都有500G左右,因此硬碟中可以存放一些比較大的檔案
    • 儲存的時長:計算機關閉之後再開啟,這些資料依舊還在,只要你不主動刪掉或者硬碟沒壞,這些資料永遠都在
    • 哪些是靜態資料:靜態資料一般是以檔案的形式儲存在硬碟上,比如檔案、照片、視訊等。
  • 動態的資料

    • 動態資料指在程式執行過程中,動態產生的臨時資料,一般儲存在記憶體中。記憶體的儲存空間一般都比較小,現在普通計算機的記憶體只有8G左右,因此要謹慎使用記憶體,不要佔用太多的記憶體空間
    • 儲存的時長:計算機關閉之後,這些臨時資料就會被清除
    • 哪些是動態資料:當執行某個程式(軟體)時,整個程式就會被載入到記憶體中,在程式執行過程中,會產生各種各樣的臨時資料,這些臨時資料都是儲存在記憶體中的。當程式停止執行或者計算機被強制關閉時,這個程式產生的所有臨時資料都會被清除。
  • 既然硬碟的儲存空間這麼大,為何不把所有的應用程式載入到硬碟中去執行呢?

    • 主要***原因就是記憶體的存取速度比硬碟快N倍***

  • 靜態資料和動態資料的相互轉換
    • 也就是從磁碟載入到記憶體
  • 動態資料和靜態資料的相互轉換
    • 也就是從記憶體儲存到磁碟
  • 資料的計量單位
    • 不管是靜態還是動態資料,都是0和1組成的
    • 資料越大,包含的0和1就越多
1 B(Byte位元組) = 8 bit(位)
// 00000000 就是一個位元組
// 111111111 也是一個位元組
// 10101010 也是一個位元組
// 任意8個0和1的組合都是一個位元組
1 KB(KByte) = 1024 B
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB

C語言資料型別

  • 作為程式設計師, 我們最關心的是記憶體中的動態資料,因為我們寫的程式就是在記憶體中執行的
  • 程式在執行過程中會產生各種各樣的臨時資料,為了方便資料的運算和操作, C語言對這些資料進行了分類, 提供了豐富的資料型別
  • C語言中有4大類資料型別:基本型別、構造型別、指標型別、空型別


什麼是常數?

  • "量"表示資料。常數,則表示一些固定的資料,也就是不能改變的資料
  • 就好比現實生活中生男生女一樣, 生下來是男孩永遠都是男孩, 生下來是女孩就永遠都是女孩, 所以性別就是現實生活中常數的一種體現
    • 不要和江哥吹牛X說你是泰國來的, 如果你真的來自泰國, 我只能說你贏了

常數的型別

  • 整型常數

    • 十進位制整數。例如:666,-120, 0
    • 八進位制整數,八進位制形式的常數都以0開頭。例如:0123,也就是十進位制的83;-011,也就是十進 制的-9
    • 十六進位制整數,十六進位制的常數都是以0x開頭。例如:0x123,也就是十進位制的291
    • 二進位制整數,逢二進一 0b開頭。例如: 0b0010,也就是十進位制的2
  • 實型常數

    • 小數形式
      • 單精度小數:以字母f或字母F結尾。例如:0.0f、1.01f
      • 雙精度小數:十進位制小數形式。例如:3.14、 6.66
      • 預設就是雙精度
      • 可以沒有整數位只有小數位。例如: .3、 .6f
    • 指數形式
      • 以冪的形式表示, 以字母e或字母E後跟一個10為底的冪數
        • 上過初中的都應該知道科學計數法吧,指數形式的常數就是科學計數法的另一種表 示,比如123000,用科學計數法表示為1.23×10的5次方
        • 用C語言表示就是1.23e5或1.23E5
        • 字母e或字母E後面的指數必須為整數
        • 字母e或字母E前後必須要有數位
        • 字母e或字母E前後不能有空格
  • 字元常數

    • 字元型常數都是用’’(單引號)括起來的。例如:‘a’、‘b’、‘c’
    • 字元常數的單引號中只能有一個字元
    • 特殊情況: 如果是跳脫字元,單引號中可以有兩個字元。例如:’\n’、’\t’
  • 字串常數

    • 字元型常數都是用""(雙引號)括起來的。例如:「a」、「abc」、「lnj」
    • 系統會自動在字串常數的末尾加一個字元’\0’作為字串結束標誌
  • 自定義常數

    • 後期講解內容, 此處先不用瞭解
  • 常數型別練習

1231.1F1.1.3‘a’「a」「李南江」

什麼是變數?

  • "量"表示資料。變數,則表示一些不固定的資料,也就是可以改變的資料
  • 就好比現實生活中人的身高、體重一樣, 隨著年齡的增長會不斷髮生改變, 所以身高、體重就是現實生活中變數的一種體現
  • 就好比現實生活中超市的儲物格一樣, 同一個格子在不同時期不同人使用,格子中儲存的物品是可以變化的。張三使用這個格子的時候裡面放的可能是尿不溼, 但是李四使用這個格子的時候裡面放的可能是麵包

如何定義變數

  • 格式1: 變數型別 變數名稱 ;
    • 為什麼要定義變數?
      • 任何變數在使用之前,必須先進行定義, 只有定義了變數才會分配儲存空間, 才有空間儲存資料
    • 為什麼要限定型別?
      • 用來約束變數所存放資料的型別。一旦給變數指明瞭型別,那麼這個變數就只能儲存這種型別的資料
      • 記憶體空間極其有限,不同型別的變數佔用不同大小的儲存空間
    • 為什麼要指定變數名稱?
      • 儲存資料的空間對於我們沒有任何意義, 我們需要的是空間中儲存的值
      • 只有有了名稱, 我們才能獲取到空間中的值
int a;
float b;
char ch;
  • 格式2:變數型別 變數名稱,變數名稱;
    • 連續定義, 多個變數之間用逗號(,)號隔開
int a,b,c;
  • 變數名的命名的規範
    • 變數名屬於識別符號,所以必須嚴格遵守識別符號的命名原則

如何使用變數?

  • 可以利用=號往變數裡面儲存資料
    • 在C語言中,利用=號往變數裡面儲存資料, 我們稱之為給變數賦值
int value;
value = 998; // 賦值
  • 注意:
    • 這裡的=號,並不是數學中的「相等」,而是C語言中的***賦值運運算元***,作用是將右邊的整型常數998賦值給左邊的整型變數value
    • 賦值的時候,= 號的左側必須是變數 (10=b,錯誤)
    • 為了方便閱讀程式碼, 習慣在 = 的兩側 各加上一個 空格

變數的初始化

  • C語言中, 變數的第一次賦值,我們稱為「初始化」
  • 初始化的兩種形式
    • 先定義,後初始化
    • int value; value = 998; // 初始化
    • 定義時同時初始化
    • int a = 10; int b = 4, c = 2;
    • 其它表現形式(不推薦)
int a, b = 10; //部分初始化
int c, d, e;
c = d = e =0;
  • 不初始化裡面儲存什麼?
    • 亂數
    • 上次程式分配的儲存空間,存數一些 內容,「垃圾」
    • 系統正在用的一些資料

如何修改變數值?

  • 多次賦值即可
    • 每次賦值都會覆蓋原來的值
int i = 10;
i = 20; // 修改變數的值

變數之間的值傳遞

  • 可以將一個變數儲存的值賦值給另一個變數
 int a = 10;
 int b = a; // 相當於把a中儲存的10拷貝了一份給b

如何檢視變數的值?

  • 使用printf輸出一個或多個變數的值
int a = 10, c = 11;
printf("a=%d, c=%d", a, c);
  • 輸出其它型別變數的值
double height = 1.75;
char blood = 'A';
printf("height=%.2f, 血型是%c", height,  blood);

變數的作用域

  • C語言中所有變數都有自己的作用域
  • 變數定義的位置不同,其作用域也不同
  • 按照作用域的範圍可分為兩種, 即區域性變數和全域性變數

  • 區域性變數
    • 區域性變數也稱為內部變數
    • 區域性變數是在***程式碼塊內***定義的, 其作用域僅限於程式碼塊內, 離開該程式碼塊後無法使用
int main(){
    int i = 998; // 作用域開始
    return 0;// 作用域結束
}
int main(){
    {
        int i = 998; // 作用域開始
    }// 作用域結束
    printf("i = %d\n", i); // 不能使用
    return 0;
}
int main(){
    {
        {
            int i = 998;// 作用域開始
        }// 作用域結束
        printf("i = %d\n", i); // 不能使用
    }
    return 0;
}

  • 全域性變數
    • 全域性變數也稱為外部變數,它是在程式碼塊外部定義的變數
int i = 666;
int main(){
    printf("i = %d\n", i); // 可以使用
    return 0;
}// 作用域結束
int call(){
    printf("i = %d\n", i); // 可以使用
    return 0;
}

  • 注意點:
    • 同一作用域範圍內不能有相同名稱的變數
int main(){
    int i = 998; // 作用域開始
    int i = 666; // 報錯, 重複定義
    return 0;
}// 作用域結束
int i = 666; 
int i = 998; // 報錯, 重複定義
int main(){
    return 0;
}
  • 不同作用域範圍內可以有相同名稱的變數
int i = 666; 
int main(){
    int i = 998; // 不會報錯
    return 0;
}
int main(){
    int i = 998; // 不會報錯
    return 0;
}
int call(){
    int i = 666;  // 不會報錯
    return 0;
}

變數記憶體分析(簡單版)

  • 位元組和地址
    • 為了更好地理解變數在記憶體中的儲存細節,先來認識一下記憶體中的「位元組」和「地址」
    • 每一個小格子代表一個位元組
    • 每個位元組都有自己的記憶體地址
    • 記憶體地址是連續的

  • 變數儲存佔用的空間
    • 一個變數所佔用的儲存空間,和***定義變數時宣告的型別***以及***當前編譯環境***有關
型別16位元編譯器32位元編譯器64位元編譯器
char111
int244
float444
double888
short222
long448
long long888
void*248
  • 變數儲存的過程
    • 根據定義變數時宣告的型別和當前編譯環境確定需要開闢多大儲存空間
    • 在記憶體中開闢一塊儲存空間,開闢時從記憶體地址大的開始開闢(記憶體定址從大到小)
    • 將資料儲存到已經開闢好的對應記憶體空間中
    int main(){
      int number;
      int value;
      number = 22;
      value = 666;
    }
    
    #include <stdio.h>
    int main(){
        int number;
        int value;
        number = 22;
        value = 666;
        printf("&number = %p\n", &number); // 0060FEAC
        printf("&value = %p\n", &value);   // 0060FEA8
    }
    

先不要著急, 剛開始接觸C語言, 我先了解這麼多就夠了. 後面會再次更深入的講解儲存的各種細節。

printf函數

  • printf函數稱之為格式輸出函數,方法名稱的最後一個字母f表示format。其功能是按照使用者指定的格式,把指定的資料輸出到螢幕上
  • printf函數的呼叫格式為:
    • printf("格式控制字串",輸出項列表 );
    • 例如:printf("a = %d, b = %d",a, b);
    • 非格式字串原樣輸出, 格式控制字串會被輸出項列表中的資料替換
    • 注意: 格式控制字串和輸出項在數量和型別上***必須一一對應***

  • 格式控制字串
    • 形式: %[標誌][輸出寬度][.精度][長度]型別

  • 型別
    • 格式: printf("a = %型別", a);
    • 型別字串用以表示輸出資料的型別, 其格式符和意義如下所示
型別含義
d有符號10進位制整型
i有符號10進位制整型
u無符號10進位制整型
o無符號8進位制整型
x無符號16進位制整型
X無符號16進位制整型
f單、雙精度浮點數(預設保留6位小數)
e / E以指數形式輸出單、雙精度浮點數
g / G以最短輸出寬度,輸出單、雙精度浮點數
c字元
s字串
p地址
#include <stdio.h>
int main(){
    int a = 10;
    int b = -10;
    float c = 6.6f;
    double d = 3.1415926;
    double e = 10.10;
    char f = 'a';
    // 有符號整數(可以輸出負數)
    printf("a = %d\n", a); // 10
    printf("a = %i\n", a); // 10

    // 無符號整數(不可以輸出負數)
    printf("a = %u\n", a); // 10
    printf("b = %u\n", b); // 429496786

    // 無符號八進位制整數(不可以輸出負數)
    printf("a = %o\n", a); // 12
    printf("b = %o\n", b); // 37777777766

    // 無符號十六進位制整數(不可以輸出負數)
    printf("a = %x\n", a); // a
    printf("b = %x\n", b); // fffffff6

    // 無符號十六進位制整數(不可以輸出負數)
    printf("a = %X\n", a); // A
    printf("b = %X\n", b); // FFFFFFF6

    // 單、雙精度浮點數(預設保留6位小數)
    printf("c = %f\n", c); // 6.600000
    printf("d = %lf\n", d); // 3.141593

    // 以指數形式輸出單、雙精度浮點數
    printf("e = %e\n", e); // 1.010000e+001
    printf("e = %E\n", e); // 1.010000E+001
    
    // 以最短輸出寬度,輸出單、雙精度浮點數
    printf("e = %g\n", e); // 10.1
    printf("e = %G\n", e); // 10.1
    
    // 輸出字元
    printf("f = %c\n", f); // a
}

  • 寬度
    • 格式: printf("a = %[寬度]型別", a);
    • 用十進位制整數來指定輸出的寬度, 如果實際位數多於指定寬度,則按照實際位數輸出, 如果實際位數少於指定寬度則以空格補位
#include <stdio.h>
int main(){
    // 實際位數小於指定寬度
    int a = 1;
    printf("a =|%d|\n", a); // |1|
    printf("a =|%5d|\n", a); // |    1|
    // 實際位數大於指定寬度
    int b = 1234567;
    printf("b =|%d|\n", b); // |1234567|
    printf("b =|%5d|\n", b); // |1234567|
}

  • 標誌
    • 格式: printf("a = %[標誌][寬度]型別", a);
標誌含義
-左對齊, 預設右對齊
+當輸出值為正數時,在輸出值前面加上一個+號, 預設不顯示
0右對齊時, 用0填充寬度.(預設用空格填充)
空格輸出值為正數時,在輸出值前面加上空格, 為負數時加上負號
#對c、s、d、u型別無影響
#對o型別, 在輸出時加字首o
#對x型別,在輸出時加字首0x
#include <stdio.h>
int main(){
    int a = 1;
    int b = -1;
    // -號標誌
    printf("a =|%d|\n", a); // |1|
    printf("a =|%5d|\n", a); // |    1|
    printf("a =|%-5d|\n", a);// |1    |
    // +號標誌
    printf("a =|%d|\n", a); // |1|
    printf("a =|%+d|\n", a);// |+1|
    printf("b =|%d|\n", b); // |-1|
    printf("b =|%+d|\n", b);// |-1|
    // 0標誌
    printf("a =|%5d|\n", a); // |    1|
    printf("a =|%05d|\n", a); // |00001|
    // 空格標誌
    printf("a =|% d|\n", a); // | 1|
    printf("b =|% d|\n", b); // |-1|
    // #號
    int c = 10;
    printf("c = %o\n", c); // 12
    printf("c = %#o\n", c); // 012
    printf("c = %x\n", c); // a
    printf("c = %#x\n", c); // 0xa
}

  • 精度
    • 格式: printf("a = %[精度]型別", a);
    • 精度格式符以"."開頭, 後面跟上十進位制整數, 用於指定需要輸出多少位小數, 如果輸出位數大於指定的精度, 則刪除超出的部分
#include <stdio.h>
int main(){
    double a = 3.1415926;
    printf("a = %.2f\n", a); // 3.14
}
  • 動態指定保留小數位數
    • 格式: printf("a = %.*f", a);
#include <stdio.h>
int main(){
    double a = 3.1415926;
    printf("a = %.*f", 2, a); // 3.14
}
  • 實型(浮點型別)有效位數問題
    • 對於單精度數,使用%f格式符輸出時,僅前6~7位是有效數位
    • 對於雙精度數,使用%lf格式符輸出時,前15~16位元是有效數位
    • 有效位數和精度(保留多少位)不同, 有效位數是指從第一個非零數位開始,誤差不超過本數位半個單位的、精確可信的數位
    • 有效位數包含小數點前的非零數位
#include <stdio.h>
int main(){
    //        1234.567871093750000
    float a = 1234.567890123456789;
    //         1234.567890123456900
    double b = 1234.567890123456789;
    printf("a = %.15f\n", a); // 前8位元數位是準確的, 後面的都不準確
    printf("b = %.15f\n", b); // 前16位元數位是準確的, 後面的都不準確
}

  • 長度
    • 格式: printf("a = %[長度]型別", a);
長度修飾型別含義
hhd、i、o、u、x輸出char
hd、i、o、u、x輸出 short int
ld、i、o、u、x輸出 long int
lld、i、o、u、x輸出 long long int
#include <stdio.h>
int main(){
    char a = 'a';
    short int b = 123;
    int  c = 123;
    long int d = 123;
    long long int e = 123;
    printf("a = %hhd\n", a); // 97
    printf("b = %hd\n", b); // 123
    printf("c = %d\n", c); // 123
    printf("d = %ld\n", d); // 123
    printf("e = %lld\n", e); // 123
}
  • 跳脫字元
    • 格式: printf("%f%%", 3.1415);
    • %號在格式控制字串中有特殊含義, 所以想輸出%必須新增一個轉移字元
#include <stdio.h>
int main(){
    printf("%f%%", 3.1415); // 輸出結果3.1415%
}

Scanf函數

  • scanf函數用於接收鍵盤輸入的內容, 是一個阻塞式函數,程式會停在scanf函數出現的地方, 直到接收到資料才會執行後面的程式碼
  • printf函數的呼叫格式為:
    • scanf("格式控制字串", 地址列表);
    • 例如: scanf("%d", &num);

  • 基本用法
    • 地址列表項中只能傳入變數地址, 變數地址可以通過&符號+變數名稱的形式獲取
#include <stdio.h>
int main(){
    int number;
    scanf("%d", &number); // 接收一個整數
    printf("number = %d\n", number); 
}
  • 接收非字元和字串型別時, 空格、Tab和回車會被忽略
#include <stdio.h>
int main(){
    float num;
    // 例如:輸入 Tab 空格 回車 回車 Tab 空格 3.14 , 得到的結果還是3.14
    scanf("%f", &num);
    printf("num = %f\n", num);
}
  • 非格式字串原樣輸入, 格式控制字串會賦值給地址項列表項中的變數
    • 不推薦這種寫法
#include <stdio.h>
int main(){
    int number;
    // 使用者必須輸入number = 數位  , 否則會得到一個意外的值
    scanf("number = %d", &number);
    printf("number = %d\n", number);
}
  • 接收多條資料
    • 格式控制字串和地址列表項在數量和型別上必須一一對應
    • 非字元和字串情況下如果沒有指定多條資料的分隔符, 可以使用空格或者回車作為分隔符(不推薦這種寫法)
    • 非字元和字串情況下建議明確指定多條資料之間分隔符
#include <stdio.h>
int main(){
    int number;
    scanf("%d", &number);
    printf("number = %d\n", number);
    int value;
    scanf("%d", &value);
    printf("value = %d\n", value);
}
#include <stdio.h>
int main(){
    int number;
    int value;
    // 可以輸入 數位 空格 數位, 或者 數位 回車 數位
    scanf("%d%d", &number, &value);
    printf("number = %d\n", number);
    printf("value = %d\n", value);
}
#include <stdio.h>
int main(){
    int number;
    int value;
    // 輸入 數位,數位 即可
    scanf("%d,%d", &number, &value);
    printf("number = %d\n", number);
    printf("value = %d\n", value);
}
  • \n是scanf函數的結束符號, 所以格式化字串中不能出現\n
#include <stdio.h>
int main(){
    int number;
    // 輸入完畢之後按下回車無法結束輸入
    scanf("%d\n", &number);
    printf("number = %d\n", number);
}

scanf執行原理

  • 系統會將使用者輸入的內容先放入輸入緩衝區
  • scanf方式會從輸入緩衝區中逐個取出內容賦值給變數
  • 如果輸入緩衝區的內容不為空,scanf會一直從緩衝區中獲取,而不要求再次輸入
#include <stdio.h>
int main(){
    int num1;
    int num2;
    char ch1;
    scanf("%d%c%d", &num1, &ch1, &num2);
    printf("num1 = %d, ch1 = %c, num2 = %d\n", num1, ch1, num2);
    char ch2;
    int num3;
    scanf("%c%d",&ch2, &num3);
    printf("ch2 = %c, num3 = %d\n", ch2, num3);
}

  • 利用fflush方法清空緩衝區(不是所有平臺都能使用)
    • 格式: fflush(stdin);
    • C和C++的標準裡從來沒有定義過 fflush(stdin)
    • MSDN 檔案裡清除的描述著"fflush on input stream is an extension to the C standard" (fflush 是在標準上擴充的函數, 不是標準函數, 所以不是所有平臺都支援)
  • 利用setbuf方法清空緩衝區(所有平臺有效)
    • 格式: setbuf(stdin, NULL);
#include <stdio.h>
int main(){
    int num1;
    int num2;
    char ch1;
    scanf("%d%c%d", &num1, &ch1, &num2);
    printf("num1 = %d, ch1 = %c, num2 = %d\n", num1, ch1, num2);
    //fflush(stdin); // 清空輸入快取區
    setbuf(stdin, NULL); // 清空輸入快取區
    char ch2;
    int num3;
    scanf("%c%d",&ch2, &num3);
    printf("ch2 = %c, num3 = %d\n", ch2, num3);
}

putchar和getchar

  • putchar: 向螢幕輸出一個字元
#include <stdio.h>
int main(){
    char ch = 'a';
    putchar(ch); // 輸出a
}
  • getchar: 從鍵盤獲得一個字元
#include <stdio.h>
int main(){
    char ch;
    ch = getchar();// 獲取一個字元
    printf("ch = %c\n", ch);
}

運運算元基本概念

  • 和數學中的運運算元一樣, C語言中的運運算元是告訴程式執行特定算術或邏輯操作的符號

    • 例如告訴程式, 某兩個數相加, 相減,相乘等
  • 什麼是表示式

    • 表示式就是利用運運算元連結在一起的有意義,有結果的語句;
    • 例如: a + b; 就是一個算數表示式, 它的意義是將兩個數相加, 兩個數相加的結果就是表示式的結果
    • 注意: 表示式一定要有結果

運運算元分類

  • 按照功能劃分:
    • 算術運運算元
    • 賦值運運算元
    • 關係運算子
    • 邏輯運運算元
    • 位運運算元
  • 按照參與運算的運算元個數劃分:
    • 單目運算
      • 只有一個運算元 如 : i++;
    • 雙目運算
      • 有兩個運算元 如 : a + b;
    • 三目運算
      • C語言中唯一的一個,也稱為問號表示式 如: a>b ? 1 : 0;

運運算元的優先順序和結合性

  • 早在小學的數學課本中,我們就學習過"從左往右,先乘除後加減,有括號的先算括號裡面的", 這句話就蘊含了優先順序和結合性的問題
  • C語言中,運運算元的運算優先順序共分為15 級。1 級最高,15 級最低
    • 在C語言表示式中,不同優先順序的運運算元, 運算次序按照由高到低執行
    • 在C語言表示式中,相同優先順序的運運算元, 運算次序按照結合性規定的方向執行

算數運運算元

優先順序名稱符號說明
3乘法運運算元*雙目運運算元,具有左結合性
3除法運運算元/雙目運運算元,具有左結合性
3求餘運運算元 (模運運算元)%雙目運運算元,具有左結合性
4加法運運算元+雙目運運算元,具有左結合性
4減法運運算元-雙目運運算元,具有左結合性
  • 注意事項
    • 如果參與運算的兩個運算元皆為整數, 那麼結果也為整數
    • 如果參與運算的兩個運算元其中一個是浮點數, 那麼結果一定是浮點數
    • 求餘運運算元, 本質上就是數學的商和餘"中的餘數
    • 求餘運運算元, 參與運算的兩個運算元必須都是整數, 不能包含浮點數
    • 求餘運運算元, 被除數小於除數, 那麼結果就是被除數
    • 求餘運運算元, 運算結果的正負性取決於被除數,跟除數無關, 被除數是正數結果就是正數,被除數是負數結果就是負數
    • 求餘運運算元, 被除數為0, 結果為0
    • 求餘運運算元, 除數為0, 沒有意義(不要這樣寫)
#include <stdio.h>
int main(){
    int a = 10;
    int b = 5;
    // 加法
    int result = a + b;
    printf("%i\n", result); // 15
    // 減法
    result = a - b;
    printf("%i\n", result); // 5
    // 乘法
    result = a * b;
    printf("%i\n", result); // 50
    // 除法
    result = a / b;
    printf("%i\n", result); // 2
    
    // 算術運運算元的結合性和優先順序
    // 結合性: 左結合性, 從左至右
    int c = 50;
    result = a + b + c; // 15 + c;  65;
    printf("%i\n", result);
    
    // 優先順序: * / % 大於 + -
    result = a + b * c; // a + 250; 260;
    printf("%i\n", result);
}
#include <stdio.h>
int main(){
    // 整數除以整數, 結果還是整數
    printf("%i\n", 10 / 3); // 3

    // 參與運算的任何一個數是小數, 結果就是小數
    printf("%f\n", 10 / 3.0); // 3.333333
}
#include <stdio.h>
int main(){
    // 10 / 3 商等於3, 餘1
    int result = 10 % 3;
    printf("%i\n", result); // 1

    // 左邊小於右邊, 那麼結果就是左邊
    result = 2 % 10;
    printf("%i\n", result); // 2

    // 被除數是正數結果就是正數,被除數是負數結果就是負數
    result = 10 % 3;
    printf("%i\n", result); // 1
    result = -10 % 3;
    printf("%i\n", result); // -1
    result = 10 % -3;
    printf("%i\n", result); // 1
}

賦值運運算元

優先順序名稱符號說明
14賦值運運算元=雙目運運算元,具有右結合性
14除後賦值運運算元/=雙目運運算元,具有右結合性
14乘後賦值運運算元 (模運運算元)*=雙目運運算元,具有右結合性
14取模後賦值運運算元%=雙目運運算元,具有右結合性
14加後賦值運運算元+=雙目運運算元,具有右結合性
14減後賦值運運算元-=雙目運運算元,具有右結合性
  • 簡單賦值運運算元
#include <stdio.h>
int main(){
    // 簡單的賦值運運算元 =
    // 會將=右邊的值賦值給左邊
    int a = 10;
    printf("a = %i\n", a); // 10
}
  • 複合賦值運運算元
#include <stdio.h>
int main(){
     // 複合賦值運運算元 += -= *= /= %=
     // 將變數中的值取出之後進行對應的操作, 操作完畢之後再重新賦值給變數
     int num1 = 10;
     // num1 = num1 + 1; num1 = 10 + 1; num1 = 11;
     num1 += 1;
     printf("num1 = %i\n", num1); // 11
     int num2 = 10;
     // num2 = num2 - 1; num2 = 10 - 1; num2 = 9;
     num2 -= 1;
     printf("num2 = %i\n", num2); // 9
     int num3 = 10;
     // num3 = num3 * 2; num3 = 10 * 2; num3 = 20;
     num3 *= 2;
     printf("num3 = %i\n", num3); // 20
     int num4 = 10;
     // num4 = num4 / 2; num4 = 10 / 2; num4 = 5;
     num4 /= 2;
     printf("num4 = %i\n", num4); // 5
     int num5 = 10;
     // num5 = num5 % 3; num5 = 10 % 3; num5 = 1;
     num5 %= 3;
     printf("num5 = %i\n", num5); // 1
}
  • 結合性和優先順序
#include <stdio.h>
int main(){
    int number = 10;
    // 賦值運運算元優先順序是14, 普通運運算元優先順序是3和4, 所以先計算普通運運算元
    // 普通運運算元中乘法優先順序是3, 加法是4, 所以先計算乘法
    // number += 1 + 25; number += 26; number = number + 26; number = 36;
    number += 1 + 5 * 5;
    printf("number = %i\n", number); // 36
}

自增自減運運算元

  • 在程式設計中,經常遇到「i=i+1」和「i=i-1」這兩種極為常用的操作。
  • C語言為這種操作提供了兩個更為簡潔的運運算元,即++和–
優先順序名稱符號說明
2自增運運算元(在後)i++單目運運算元,具有左結合性
2自增運運算元(在前)++i單目運運算元,具有右結合性
2自減運運算元(在後)i–單目運運算元,具有左結合性
2自減運運算元(在前)–i單目運運算元,具有右結合性

  • 自增
    • 如果只有***單個***變數, 無論++寫在前面還是後面都會對變數做+1操作
#include <stdio.h>
int main(){
    int number = 10;
    number++;
    printf("number = %i\n", number); // 11
    ++number;
    printf("number = %i\n", number); // 12
}
  • 如果出現在一個表示式中, 那麼++寫在前面和後面就會有所區別
    • 字首表示式:++x, --x;其中x表示變數名,先完成變數的自增自減1運算,再用x的值作為表示式的值;即「先變後用」,也就是變數的值先變,再用變數的值參與運算
    • 字尾表示式:x++, x–;先用x的當前值作為表示式的值,再進行自增自減1運算。即「先用後變」,也就是先用變數的值參與運算,變數的值再進行自增自減變化
#include <stdio.h>
int main(){
    int number = 10;
    // ++在後, 先參與表示式運算, 再自增
    // 表示式運算時為: 3 + 10;
    int result = 3 + number++;
    printf("result = %i\n", result); // 13
    printf("number = %i\n", number); // 11
}
#include <stdio.h>
int main(){
    int number = 10;
    // ++在前, 先自增, 再參與表示式運算
    // 表示式運算時為: 3 + 11;
    int result = 3 + ++number;
    printf("result = %i\n", result); // 14
    printf("number = %i\n", number); // 11
}
  • 自減
#include <stdio.h>
int main(){
    int number = 10;
    // --在後, 先參與表示式運算, 再自減
    // 表示式運算時為: 10 + 3;
    int result = number-- + 3;
    printf("result = %i\n", result); // 13
    printf("number = %i\n", number); // 9
}
#include <stdio.h>
int main(){
    int number = 10;
    // --在前, 先自減, 再參與表示式運算
    // 表示式運算時為: 9 + 3;
    int result = --number + 3;
    printf("result = %i\n", result); // 12
    printf("number = %i\n", number); // 9
}
  • 注意點:
    • 自增、自減運算只能用於單個變數,只要是標準型別的變數,不管是整型、實型,還是字元型變數等,但不能用於表示式或常數
      • 錯誤用法: ++(a+b); 5++;
    • 企業開發中儘量讓++ – 單獨出現, 儘量不要和其它運運算元混合在一起
int i = 10;
int b = i++; // 不推薦
或者
int b = ++i; // 不推薦
或者
int a = 10;
int b = ++a + a++;  // 不推薦
  • 請用如下程式碼替代
int i = 10;
int b = i; // 推薦
i++;
或者;
i++;
int b = i; // 推薦
或者
int a = 10;
++a;
int b = a + a; // 推薦
a++;
  • C語言標準沒有明確的規定,同一個表示式中同一個變數自增或自減後如何運算, 不同編譯器得到結果也不同, 在企業開發中千萬不要這樣寫
    int a = 1;
    // 下列程式碼利用Qt執行時6, 利用Xcode執行是5
    // 但是無論如何, 最終a的值都是3
   //  在C語言中這種程式碼沒有意義, 不用深究也不要這樣寫
   // 特點: 參與運算的是同一個變數, 參與運算時都做了自增自減操作, 並且在同一個表示式中
    int b = ++a + ++a;
    printf("b = %i\n", b); 

sizeof運運算元

  • sizeof可以用來計算一個變數或常數、資料型別所佔的記憶體位元組數

    • 標準格式: sizeof(常數 or 變數);
  • sizeof的幾種形式

    • sizeof( 變數\常數 );
      • sizeof(10);
      • char c = 'a'; sizeof(c);
    • sizeof 變數\常數;
      • sizeof 10;
      • char c = 'a'; sizeof c;
    • sizeof( 資料型別);
      • sizeof(float);
      • 如果是資料型別不能省略括號
  • sizeof面試題:

    • sizeof()和+=、*=一樣是一個複合運運算元, 由sizeof和()兩個部分組成, 但是代表的是一個整體
    • 所以sizeof不是一個函數, 是一個運運算元, 該運運算元的優先順序是2
#include <stdio.h>
int main(){
    int a = 10;
    double b = 3.14;
    // 由於sizeof的優先順序比+號高, 所以會先計算sizeof(a);
    // a是int型別, 所以佔4個位元組得到結果4
    // 然後再利用計算結果和b相加, 4 + 3.14 = 7.14
    double res = sizeof a+b;
    printf("res = %lf\n", res); // 7.14
}

逗號運運算元

  • 在C語言中逗號「,」也是一種運運算元,稱為逗號運運算元。 其功能是把多個表示式連線起來組成一個表示式,稱為逗號表示式
  • 逗號運運算元會從左至右依次取出每個表示式的值, 最後整個逗號表示式的值等於最後一個表示式的值
  • 格式: 表示式1,表示式2,… …,表示式n;
    • 例如: int result = a+1,b=3*4;
#include <stdio.h>
int main(){
    int a = 10, b = 20, c;
    // ()優先順序高於逗號運運算元和賦值運運算元, 所以先計算()中的內容
    // c = (11, 21);
    // ()中是一個逗號表示式, 結果是最後一個表示式的值, 所以計算結果為21
    // 將逗號表示式的結果賦值給c, 所以c的結果是21
    c = (a + 1, b + 1);
    printf("c = %i\n", c); // 21
}

關係運算子

  • 為什麼要學習關係運算子
    • 預設情況下,我們在程式中寫的每一句正確程式碼都會被執行。但很多時候,我們想在某個條件成立的情況下才執行某一段程式碼
    • 這種情況的話可以使用條件語句來完成,但是學習條件語句之前,我們先來看一些更基礎的知識:如何判斷一個條件是否成立

  • C語言中的真假性
    • 在C語言中,條件成立稱為「真」,條件不成立稱為「假」,因此,判斷條件是否成立,就是判斷條件的「真假」
    • 怎麼判斷真假呢?C語言規定,任何數值都有真假性,任何非0值都為「真」,只有0才為「假」。也就是說,108、-18、4.5、-10.5等都是「真」,0則是「假」

  • 關係運算子的運算結果只有2種:如果條件成立,結果就為1,也就是「真」;如果條件不成立,結果就為0,也就是「假」
優先順序名稱符號說明
6大於運運算元>雙目運運算元,具有左結合性
6小於運運算元<雙目運運算元,具有左結合性
6大於等於運運算元>=雙目運運算元,具有左結合性
6小於等於運運算元<=雙目運運算元,具有左結合性
7等於運運算元==雙目運運算元,具有左結合性
7不等於運運算元!=雙目運運算元,具有左結合性
#include <stdio.h>
int main(){
    int result = 10 > 5;
    printf("result = %i\n", result); // 1
    result = 5 < 10;
    printf("result = %i\n", result); // 1
    result = 5 > 10;
    printf("result = %i\n", result); // 0
    result = 10 >= 10;
    printf("result = %i\n", result); // 1
    result = 10 <= 10;
    printf("result = %i\n", result); // 1
    result = 10 == 10;
    printf("result = %i\n", result); // 1
    result = 10 != 9;
    printf("result = %i\n", result); // 1
}
  • 優先順序和結合性
#include <stdio.h>
int main(){
    // == 優先順序 小於 >, 所以先計算>
    // result = 10 == 1; result = 0;
    int result = 10 == 5 > 3;
    printf("result = %i\n", result); // 0
}
#include <stdio.h>
int main(){
    // == 和 != 優先順序一樣, 所以按照結合性
    // 關係運算子是左結合性, 所以從左至右計算
    // result = 0 != 3; result = 1;
    int result = 10 == 5 != 3;
    printf("result = %i\n", result); // 1
}
  • 練習: 計算result的結果
int result1 = 3 > 4 + 7
int result2 = (3>4) + 7
int result3 = 5 != 4 + 2 * 7 > 3 == 10
  • 注意點:
    • 無論是float還是double都有精度問題, 所以一定要避免利用==判斷浮點數是否相等
#include <stdio.h>
int main(){
    float a = 0.1;
    float b = a * 10 + 0.00000000001;
    double c = 1.0 + + 0.00000000001;
    printf("b = %f\n", b);
    printf("c = %f\n", c);
    int result = b == c;
    printf("result = %i\n", result); // 0
}

邏輯運運算元

優先順序名稱符號說明
2邏輯非運運算元!單目運運算元,具有右結合性
11邏輯與運運算元&&雙目運運算元,具有左結合性
12邏輯或運運算元\|\|雙目運運算元,具有左結合性
  • 邏輯非
    • 格式: ! 條件A;
    • 運算結果: 真變假,假變真
    • 運算過程:
      • 先判斷條件A是否成立,如果新增A成立, 那麼結果就為0,即「假」;
      • 如果條件A不成立,結果就為1,即「真」
    • 使用注意:
      • 可以多次連續使用邏輯非運運算元
      • !!!0;相當於(!(!(!0)));最終結果為1
#include <stdio.h>
int main(){
    // ()優先順序高, 先計算()裡面的內容
    // 10==10為真, 所以result = !(1);
    // !代表真變假, 假變真,所以結果是假0
    int result = !(10 == 10);
    printf("result = %i\n", result); // 0
}

  • 邏輯與
    • 格式: 條件A && 條件B;
    • 運算結果:一假則假
    • 運算過程:
      • 總是先判斷"條件A"是否成立
      • 如果"條件A"成立,接著再判斷"條件B"是否成立, 如果"條件B"也成立,結果就為1,即「真」
      • 如果"條件A"成立,"條件B"不成立,結果就為0,即「假」
      • 如果"條件A"不成立,不會再去判斷"條件B"是否成立, 因為邏輯與只要一個不為真結果都不為真
    • 使用注意:
      • "條件A"為假, "條件B"不會被執行
#include <stdio.h>
int main(){
    //               真     &&    真
    int result = (10 == 10) && (5 != 1);
    printf("result = %i\n", result); // 1
    //          假     &&    真
    result = (10 == 9) && (5 != 1);
    printf("result = %i\n", result); // 0
    //          真     &&    假
    result = (10 == 10) && (5 != 5);
    printf("result = %i\n", result); // 0
    //          假     &&    假
    result = (10 == 9) && (5 != 5);
    printf("result = %i\n", result); // 0
}
#include <stdio.h>
int main(){
    int a = 10;
    int b = 20;
    // 邏輯與, 前面為假, 不會繼續執行後面
    int result = (a == 9) && (++b);
    printf("result = %i\n", result); // 1
    printf("b = %i\n", b); // 20
}

  • 邏輯或
    • 格式: 條件A || 條件B;
    • 運算結果:一真則真
    • 運算過程:
      • 總是先判斷"條件A"是否成立
      • 如果"條件A"不成立,接著再判斷"條件B"是否成立, 如果"條件B"成立,結果就為1,即「真」
      • 如果"條件A"不成立,"條件B"也不成立成立, 結果就為0,即「假」
      • 如果"條件A"成立, 不會再去判斷"條件B"是否成立, 因為邏輯或只要一個為真結果都為真
    • 使用注意:
      • "條件A"為真, "條件B"不會被執行
#include <stdio.h>
int main(){
    //               真     ||    真
    int result = (10 == 10) || (5 != 1);
    printf("result = %i\n", result); // 1
    //          假     ||    真
    result = (10 == 9) || (5 != 1);
    printf("result = %i\n", result); // 1
    //          真     ||    假
    result = (10 == 10) || (5 != 5);
    printf("result = %i\n", result); // 1
    //          假     ||    假
    result = (10 == 9) || (5 != 5);
    printf("result = %i\n", result); // 0
}
#include <stdio.h>
int main(){
    int a = 10;
    int b = 20;
    // 邏輯或, 前面為真, 不會繼續執行後面
    int result = (a == 10) || (++b);
    printf("result = %i\n", result); // 1
    printf("b = %i\n", b); // 20
}
  • 練習: 計算result的結果
int result = 3>5 || 2<4 && 6<1;

三目運運算元

  • 三目運運算元,它需要3個資料或表示式構成條件表示式

  • 格式: 表示式1?表示式2(結果A):表示式3(結果B)

    • 範例: 考試及格 ? 及格 : 不及格;
  • 求值規則:

    • 如果"表示式1"為真,三目運運算元的運算結果為"表示式2"的值(結果A),否則為"表示式3"的值(結果B)
範例:
    int a = 10;
    int b = 20;
    int max = (a > b) ? a : b;
    printf("max = %d", max);
    輸出結果: 20
等價於:
    int a = 10;
    int b = 20;
    int max = 0;
    if(a>b){
      max=a;
    }else {
       max=b;
    }
    printf("max = %d", max);
  • 注意點
    • 條件運運算元的運算優先順序低於關係運算子和算術運運算元,但高於賦值符
    • 條件運運算元?和:是一個整體,不能分開使用
#include <stdio.h>
int main(){
    int a = 10;
    int b = 5;
    // 先計算 a > b
    // 然後再根據計算結果判定返回a還是b
    // 相當於int max= (a>b) ? a : b;
    int max= a>b ? a : b;
    printf("max = %i\n", max); // 10
}
#include <stdio.h>
int main(){
    int a = 10;
    int b = 5;
    int c = 20;
    int d = 10;
    // 結合性是從右至左, 所以會先計算:後面的內容
    // int res = a>b?a:(c>d?c:d);
    // int res = a>b?a:(20>10?20:10);
    // int res = a>b?a:(20);
    // 然後再計算最終的結果
    // int res = 10>5?10:(20);
    // int res = 10;
    int res = a>b?a:c>d?c:d;
    printf("res = %i\n", res);
}

型別轉換

強制型別轉換(顯示轉換)自動型別轉換(隱式轉換)
(需要轉換的型別)(表示式)1.算數轉換 2.賦值轉換
  • 強制型別轉換(顯示轉換)
// 將double轉換為int
int a = (int)10.5;
  • 算數轉換
    • 系統會自動對佔用記憶體較少的型別做一個「自動型別提升」的操作, 先將其轉換為當前算數表示式中佔用記憶體高的型別, 然後再參與運算
// 當前表示式用1.0佔用8個位元組, 2佔用4個位元組
// 所以會先將整數型別2轉換為double型別之後再計算
double b = 1.0 / 2;
  • 賦值轉換
// 賦值時左邊是什麼型別,就會自動將右邊轉換為什麼型別再儲存
int a = 10.6;
  • 注意點:
    • 參與計算的是什麼型別, 結果就是什麼型別
// 結果為0, 因為參與運算的都是整型
double a = (double)(1 / 2);
// 結果為0.5, 因為1被強制轉換為了double型別, 2也會被自動提升為double型別
double b = (double)1 / 2;
  • 型別轉換並不會影響到原有變數的值
#include <stdio.h>
int main(){
    double d = 3.14;
    int num = (int)d;
    printf("num = %i\n", num); // 3
    printf("d = %lf\n", d); // 3.140000
}

階段練習

  • 從鍵盤輸入一個整數, 判斷這個數是否是100到200之間的數
  • 表示式 6==6==6 的值是多少?
  • 使用者從鍵盤上輸入三個整數,找出最大值,然後輸入最大值
  • 用兩種方式交換兩個變數的儲存的值
交換前
int a = 10; int b = 20;
交換後
int a = 20; int b = 10;

流程控制基本概念

  • 預設情況下程式執行後,系統會按書寫順序從上至下依次執行程式中的每一行程式碼。但是這並不能滿足我們所有的開發需求, 為了方便我們控制程式的執行流程,C語言提供3種流程控制結構,不同的流程控制結構可以實現不同的執行流程。

  • 這3種流程結構分別是順序結構、選擇結構、迴圈結構

  • 順序結構:

    • 按書寫順序從上至下依次執行
  • 選擇結構

    • 對給定的條件進行判斷,再根據判斷結果來決定執行程式碼

  • 迴圈結構

    • 在給定條件成立的情況下,反覆執行某一段程式碼


選擇結構

  • C語言中提供了兩大選擇結構, 分別是if和switch
    ##選擇結構if
  • if第一種形式
    • 表示如果表示式為真,執行語句塊1,否則不執行
if(表示式) {
  語句塊1;
}
後續語句;
if(age >= 18) {
  printf("開網路卡\n");
}
printf("買菸\n");
  • if第二種形式
    • 如果表示式為真,則執行語句塊1,否則執行語句塊2
    • else不能脫離if單獨使用
if(表示式){
  語句塊1;
}else{
  語句塊2;
}
後續語句;
if(age > 18){
  printf("開網路卡\n");
}else{
  printf("喊家長來開\n");
}
printf("買菸\n");
  • if第三種形式
    • 如果"表示式1"為真,則執行"語句塊1",否則判斷"表示式2",如果為真執行"語句塊2",否則再判斷"表示式3",如果真執行"語句塊3", 當表示式1、2、3都不滿足,會執行最後一個else語句
    • 眾多大括號中,只有一個大括號中的內容會被執行
    • 只有前面所有新增都不滿足, 才會執行else大括號中的內容
if(表示式1) {
  語句塊1;
}else if(表示式2){
  語句塊2;
}else if(表示式3){
  語句塊3;
}else{
  語句塊4;
}
後續語句;
if(age>40){
  printf("給房卡");
}else if(age>25){
  printf("給名片");
}else if(age>18){
   printf("給網路卡");
}else{
  printf("給好人卡");
}
printf("買菸\n");
  • if巢狀
    • if中可以繼續巢狀if, else中也可以繼續巢狀if
if(表示式1){
    語句塊1;
   if(表示式2){
      語句塊2;
  }
}else{
   if(表示式3){
      語句塊3;
  }else{
      語句塊4;
  }
}

  • if注意點
    • 任何數值都有真假性
#include <stdio.h>
int main(){
    if(0){
        printf("執行了if");
    }else{
        printf("執行了else"); // 被執行
    }
}
  • 當if else後面只有一條語句時, if else後面的大括號可以省略
    // 極其不推薦寫法
    int age = 17;
    if (age >= 18)
        printf("開網路卡\n");
    else
        printf("喊家長來開\n");
  • 當if else後面的大括號被省略時, else會自動和距離最近的一個if匹配
#include <stdio.h>
int main(){
    if(0)
    if(1)
    printf("A\n");
    else // 和if(1)匹配
    printf("B\n");
    else // 和if(0)匹配, 因為if(1)已經被匹配過了
    if (1)
    printf("C\n"); // 輸出C
    else // 和if(1)匹配
    printf("D\n");
}
    • 如果if else省略了大括號, 那麼後面不能定義變數
#include <stdio.h>
int main(){
    if(1)
        int number = 10; // 系統會報錯
    printf("number = %i\n", number);
}
#include <stdio.h>
int main(){
    if(0){
        int number = 10; 
    }else
        int value = 20; // 系統會報錯
    printf("value = %i\n", value);
}
  • C語言中分號(;)也是一條語句, 稱之為空語句
// 因為if(10 > 2)後面有一個分號, 所以系統會認為if省略了大括號
// if省略大括號時只能管控緊隨其後的那條語句, 所以只能管控分號
if(10 > 2);
{
printf("10 > 2");
}
// 輸出結果: 10 > 2
  • 但凡遇到比較一個變數等於或者不等於某一個常數的時候,把常數寫在前面
#include <stdio.h>
int main(){
    int a = 8;
//    if(a = 10){// 錯誤寫法, 但不會報錯
    if (10 == a){
      printf("a的值是10\n");
    }else{
     printf("a的值不是10\n");
    }
}

  • if練習

    • 從鍵盤輸入一個整數,判斷其是否是偶數,如果是偶數就輸出YES,否則輸出NO;
    • 接收使用者輸入的1~7的整數,根據使用者輸入的整數,輸出對應的星期幾
    • 接收使用者輸入的一個整數month代表月份,根據月份輸出對應的季節
    • 接收使用者輸入的兩個整數,判斷大小後輸出較大的那個數
    • 接收使用者輸入的三個整數,判斷大小後輸出較大的那個數
    • 接收使用者輸入的三個整數,排序後輸出
  • 實現石頭剪刀布

剪刀石頭布遊戲:
1)定義遊戲規則
  剪刀 幹掉 布
  石頭 幹掉 剪刀
  布 幹掉石頭
2)顯示玩家開始猜拳
3)接收玩家輸入的內容
4)讓電腦隨機產生一種拳
5)判斷比較
(1)玩家贏的情況(顯示玩家贏了)
(2)電腦贏的情況(顯示電腦贏了)
(3)平局(顯示平局)




選擇結構switch

  • 由於 if else if 還是不夠簡潔,所以switch 就應運而生了,他跟 if else if 互為補充關係。switch 提供了點的多路選擇
  • 格式:
switch(表示式){
    case 常數表示式1:
        語句1;
        break;
    case 常數表示式2:
        語句2; 
        break;
    case 常數表示式n:
        語句n;
        break;
    default:
        語句n+1;
        break;
}
  • 語意:
    • 計算"表示式"的值, 逐個與其後的"常數表示式"值相比較,當"表示式"的值與某個"常數表示式"的值相等時, 即執行其後的語句, 然後跳出switch語句
    • 如果"表示式"的值與所有case後的"常數表示式"均不相同時,則執行default後的語句
  • 範例:
#include <stdio.h>

int main() {

    int num = 3;
    switch(num){
    case 1:
        printf("星期一\n");
        break;
    case 2:
        printf("星期二\n");
        break;
    case 3:
        printf("星期三\n");
        break;
    case 4:
        printf("星期四\n");
        break;
    case 5:
        printf("星期五\n");
        break;
    case 6:
        printf("星期六\n");
        break;
    case 7:
        printf("星期日\n");
        break;
    default:
        printf("回火星去\n");
        break;
    }
}

  • switch注意點
    • switch條件表示式的型別必須是整型, 或者可以被提升為整型的值(char、short)
#include <stdio.h>

int main() {

    switch(1.1){ // 報錯
    case 1:
        printf("星期一\n");
        break;
    case 2:
        printf("星期二\n");
        break;
    default:
        printf("回火星去\n");
        break;
    }
}
  • +case的值只能是常數, 並且還必須是整型, 或者可以被提升為整型的值(char、short)
#include <stdio.h>

int main() {

    int num = 3;
    switch(1){ 
    case 1:
        printf("星期一\n");
        break;
    case 'a':
        printf("星期二\n");
        break;
    case num: // 報錯
        printf("星期三\n");
        break;
    case 4.0: // 報錯
        printf("星期四\n");
        break;
    default:
        printf("回火星去\n");
        break;
    }
}
  • case後面常數表示式的值不能相同
#include <stdio.h>

int main() {
    switch(1){ 
    case 1: // 報錯
        printf("星期一\n");
        break;
    case 1: // 報錯
        printf("星期一\n");
        break;
    default:
        printf("回火星去\n");
        break;
    }
}
  • case後面要想定義變數,必須給case加上大括號
#include <stdio.h>

int main() {
    switch(1){
    case 1:{
        int num = 10;
        printf("num = %i\n", num);
        printf("星期一\n");
        break;
        }
    case 2:
        printf("星期一\n");
        break;
    default:
        printf("回火星去\n");
        break;
    }
}
  • switch中只要任意一個case匹配, 其它所有的case和default都會失效. 所以如果case和default後面沒有break就會出現穿透問題
#include <stdio.h>

int main() {

    int num = 2;
    switch(num){
    case 1:
        printf("星期一\n");
        break;
    case 2:
        printf("星期二\n"); // 被輸出
    case 3:
        printf("星期三\n"); // 被輸出
    default:
        printf("回火星去\n"); // 被輸出
        break;
    }
}
  • switch中default可以省略
#include <stdio.h>

int main() {
    switch(1){
    case 1:
        printf("星期一\n");
        break;
    case 2:
        printf("星期一\n");
        break;
    }
}
  • switch中default的位置不一定要寫到最後, 無論放到哪都會等到所有case都不匹配才會執行(穿透問題除外)
#include <stdio.h>

int main() {
    switch(3){
    case 1:
        printf("星期一\n");
        break;
    default:
        printf("Other,,,\n");
        break;
    case 2:
        printf("星期一\n");
        break;
    }
}

  • if和Switch轉換
  • 看上去if和switch都可以實現同樣的功能, 那麼在企業開發中我們什麼時候使用if, 什麼時候使用switch呢?
    • if else if 針對於範圍的多路選擇
    • switch 是針對點的多路選擇
  • 判斷使用者輸入的資料是否大於100
#include <stdio.h>

int main() {
    int a = -1;
    scanf("%d", &a);
    if(a > 100){
        printf("使用者輸入的資料大於100");
    }else{
        printf("使用者輸入的資料不大於100");
    }
}
#include <stdio.h>

int main() {
    int a = -1;
    scanf("%d", &a);
    // 挺(T)萌(M)的(D)搞不定啊
    switch (a) {
        case 101:
        case 102:
        case 103:
        case 104:
        case 105:
            printf("大於\n");
            break;
        default:
            printf("不大於\n");
            break;
    }
}

  • 練習
    • 實現分數等級判定
要求使用者輸入一個分數,根據輸入的分數輸出對應的等級
A 90~100  
B 80~89
C 70~79
D 60~69
E 0~59
  • 實現+ - * / 簡單計算器

迴圈結構

  • C語言中提供了三大回圈結構, 分別是while、dowhile和for
  • 迴圈結構是程式中一種很重要的結構。
    • 其特點是,在給定條件成立時,反覆執行某程式段, 直到條件不成立為止。
    • 給定的條件稱為"迴圈條件",反覆執行的程式段稱為"迴圈體"

迴圈結構while

  • 格式:
while (  迴圈控制條件 ) {
    迴圈體中的語句;
    能夠讓迴圈結束的語句;
    ....
}
  • 構成迴圈結構的幾個條件

    • 迴圈控制條件
      • 迴圈退出的主要依據,來控制迴圈到底什麼時候退出
    • 迴圈體
      • 迴圈的過程中重複執行的程式碼段
    • 能夠讓迴圈結束的語句(遞增、遞減、真、假等)
      • 能夠讓迴圈條件為假的依據,否則退出迴圈
  • 範例:

int count = 0;
while (count < 3) { // 迴圈控制條件
    printf("發射子彈~嗶嗶嗶嗶\n"); // 需要反覆執行的語句
    count++; // 能夠讓迴圈結束的語句
}
  • while迴圈執行流程
    • 首先會判定"迴圈控制條件"是否為真, 如果為假直接跳到迴圈語句後面
    • 如果"迴圈控制條件"為真, 執行一次迴圈體, 然後再次判斷"迴圈控制條件"是否為真, 為真繼續執行迴圈體,為假跳出迴圈
    • 重複以上操作, 直到"迴圈控制條件"為假為止
#include <stdio.h>
int main(){
    int count = 4;
    // 1.判斷迴圈控制條件是否為真,此時為假所以跳過迴圈語句
    while (count < 3) { 
        printf("發射子彈~嗶嗶嗶嗶\n"); 
        count++; 
    }
    // 2.執行迴圈語句後面的程式碼, 列印"迴圈執行完畢"
    printf("迴圈執行完畢\n");
}
#include <stdio.h>
int main(){
    int count = 0;
    // 1.判斷迴圈控制條件是否為真,此時0 < 3為真
    // 4.再次判斷迴圈控制條件是否為真,此時1 < 3為真
    // 7.再次判斷迴圈控制條件是否為真,此時2 < 3為真
    // 10.再次判斷迴圈控制條件是否為真,此時3 < 3為假, 跳過迴圈語句
    while (count < 3) { 
        // 2.執行迴圈體中的程式碼, 列印"發子彈"
        // 5.執行迴圈體中的程式碼, 列印"發子彈"
        // 8.執行迴圈體中的程式碼, 列印"發子彈"
        printf("發射子彈~嗶嗶嗶嗶\n"); 
        // 3.執行"能夠讓迴圈結束的語句" count = 1
        // 6.執行"能夠讓迴圈結束的語句" count = 2
        // 9.執行"能夠讓迴圈結束的語句" count = 3
        count++; 
    }
    // 11.執行迴圈語句後面的程式碼, 列印"迴圈執行完畢"
    printf("迴圈執行完畢\n");
}

  • while迴圈注意點
    • 任何數值都有真假性
#include <stdio.h>
int main(){
    while (1) { // 死迴圈
         printf("發射子彈~嗶嗶嗶嗶\n");
         // 沒有能夠讓迴圈結束的語句
    }
}
  • 當while後面只有一條語句時,while後面的大括號可以省略
#include <stdio.h>
int main(){
    while (1)  // 死迴圈
         printf("發射子彈~嗶嗶嗶嗶\n");
         // 沒有能夠讓迴圈結束的語句
}
  • 如果while省略了大括號, 那麼後面不能定義變數
#include <stdio.h>
int main(){
    while (1)  // 死迴圈
         int num = 10; // 報錯
         // 沒有能夠讓迴圈結束的語句
}
  • C語言中分號(;)也是一條語句, 稱之為空語句
#include <stdio.h>
int main(){
    int count = 0;
    while (count < 3);{ // 死迴圈
       printf("發射子彈~嗶嗶嗶嗶\n"); 
       count++; 
    }
}
  • 最簡單的死迴圈
// 死迴圈一般在作業系統級別的應用程式會比較多, 日常開發中很少用
while (1);

  • while練習
    • 計算1 + 2 + 3 + …n的和
    • 獲取1~100之間 7的倍數的個數

迴圈結構do while

  • 格式:
do {
    迴圈體中的語句;
    能夠讓迴圈結束的語句;
    ....
} while (迴圈控制條件 );
  • 範例
int count = 0;
do {
   printf("發射子彈~嗶嗶嗶嗶\n");
   count++;
}while(count < 10);
  • do-while迴圈執行流程

    • 首先不管while中的條件是否成立, 都會執行一次"迴圈體"
    • 執行完一次迴圈體,接著再次判斷while中的條件是否為真, 為真繼續執行迴圈體,為假跳出迴圈
    • 重複以上操作, 直到"迴圈控制條件"為假為止
  • 應用場景

    • 口令校驗
#include<stdio.h>
int main()
{
    int num = -1;
    do{
        printf("請輸入密碼,驗證您的身份\n");
        scanf("%d", &num);
    }while(123456 != num);
    printf("主人,您終於回來了\n");
}
  • while和dowhile應用場景
    • 絕大多數情況下while和dowhile可以互換, 所以能用while就用while
    • 無論如何都需要先執行一次迴圈體的情況, 才使用dowhile
    • do while 曾一度提議廢除,但是他在輸入性檢查方面還是有點用的

迴圈結構for

  • 格式:
for(初始化表示式;迴圈條件表示式;迴圈後的操作表示式) {
    迴圈體中的語句;
}
  • 範例
for(int i = 0; i < 10; i++){
    printf("發射子彈~嗶嗶嗶嗶\n");
}
  • for迴圈執行流程

    • 首先執行"初始化表示式",而且在整個迴圈過程中,***只會執行一次***初始化表示式
    • 接著判斷"迴圈條件表示式"是否為真,為真執行迴圈體中的語句
    • 迴圈體執行完畢後,接下來會執行"迴圈後的操作表示式",然後再次判斷條件是否為真,為真繼續執行迴圈體,為假跳出迴圈
    • 重複上述過程,直到條件不成立就結束for迴圈
  • for迴圈注意點:

    • 和while一模一樣
    • 最簡單的死迴圈for(;;);
  • for和while應用場景

    • while能做的for都能做, 所以企業開發中能用for就用for, 因為for更為靈活
    • 而且對比while來說for更節約記憶體空間
int count = 0; // 初始化表示式
while (count < 10) { // 條件表示式
      printf("發射子彈~嗶嗶嗶嗶 %i\n", count);
      count++; // 迴圈後增量表示式
}
// 如果初始化表示式的值, 需要在迴圈之後使用, 那麼就用while
printf("count = %i\n", count);
// 注意: 在for迴圈初始化表示式中定義的變數, 只能在for迴圈後面的{}中存取
// 所以: 如果初始化表示式的值, 不需要在迴圈之後使用, 那麼就用for
// 因為如果初始化表示式的值, 在迴圈之後就不需要使用了 , 那麼用while會導致效能問題
for (int count = 0; count < 10; count++) {
     printf("發射子彈~嗶嗶嗶嗶 %i\n", count);
}
//     printf("count = %i\n", count);
// 如果需要使用初始化表示式的值, 也可以將初始化表示式寫到外面
int count = 0;
for (; count < 10; count++) {
     printf("發射子彈~嗶嗶嗶嗶\n", count);
}
printf("count = %i\n", count);

四大跳轉

  • C語言中提供了四大跳轉語句, 分別是return、break、continue、goto

  • break:

    • 立即跳出switch語句或迴圈
  • 應用場景:

    • switch
    • 迴圈結構

  • break注意點:

    • break離開應用範圍,存在是沒有意義的
if(1) {
  break; // 會報錯
}
  • 在多層迴圈中,一個break語句只向外跳一層
while(1) {
  while(2) {
    break;// 只對while2有效, 不會影響while1
  }
  printf("while1迴圈體\n");
}
  • break下面不可以有語句,因為執行不到
while(2){
  break;
  printf("打我啊!");// 執行不到
}

  • continue
    • 結束***本輪***迴圈,進入***下一輪***迴圈
  • 應用場景:
    • 迴圈結構
  • continue注意點:
    • continue離開應用範圍,存在是沒有意義的
if(1) {
  continue; // 會報錯
}

  • goto
    • 這是一個不太值得探討的話題,goto 會破壞結構化程式設計流程,它將使程式層次不清,且不易讀,所以慎用
    • goto 語句,僅能在本函數內實現跳轉,不能實現跨函數跳轉(短跳轉)。但是他在跳出多重回圈的時候效率還是蠻高的
#include <stdio.h>
int main(){
    int num = 0;
// loop:是定義的標記
loop:if(num < 10){
        printf("num = %d\n", num);
        num++;
        // goto loop代表跳轉到標記的位置
        goto loop;
    }
}
#include <stdio.h>
int main(){
    while (1) {
        while(2){
            goto lnj;
        }
    }
    lnj:printf("跳過了所有迴圈");
}

  • return
    • 結束當前函數,將結果返回給呼叫者
    • 不著急, 放一放,學到函數我們再回頭來看它

迴圈的巢狀

  • 迴圈結構的迴圈體中存在其他的迴圈結構,我們稱之為迴圈巢狀
    • 注意: 一般迴圈巢狀不超過三層
    • 外迴圈執行的次數 * 內迴圈執行的次數就是內迴圈總共執行的次數
  • 格式:
while(條件表示式) {
    while迴圈結構 or dowhile迴圈結構 or for迴圈結構
}
for(初始化表示式;迴圈條件表示式;迴圈後的操作表示式) {
    while迴圈結構 or dowhile迴圈結構 or for迴圈結構
}
do {
     while迴圈結構 or dowhile迴圈結構 or for迴圈結構
} while (迴圈控制條件 );
  • 迴圈優化
    • 在多重回圈中,如果有可能,應當將最長的迴圈放在最內層,最短的迴圈放在最外層,以減少 CPU 跨切迴圈層的次數
for (row=0; row<100; row++) {
  // 低效率:長迴圈在最外層
  for ( col=0; col<5; col++ ) {
    sum = sum + a[row][col];
  }
}
for (col=0; col<5; col++ ) {
  // 高效率:長迴圈在最內層
  for (row=0; row<100; row++) {
    sum = sum + a[row][col];
  }
}
  • 練習
    • 列印好友列表
好友列表1
    好友1
    好友2
好友列表2
    好友1
    好友2
好友列表3
    好友1
    好友2
for (int i = 0; i < 4; i++) {
    printf("好友列表%d\n", i+1);
    for (int j = 0; j < 4; j++) {
        printf("    角色%d\n", j);
    }
}

圖形列印

  • 一重回圈解決線性的問題,而二重回圈和三重回圈就可以解決平面和立體的問題了
  • 列印矩形
****
****
****
// 3行4列
//  外迴圈控制行數
for (int i = 0; i < 3; i++) {
//        內迴圈控制列數
    for (int j = 0; j < 4; j++) {
        printf("*");
    }
    printf("\n");
}
  • 列印三角形
    • 尖尖朝上,改變內迴圈的條件表示式,讓內迴圈的條件表示式隨著外迴圈的i值變化
    • 尖尖朝下,改變內迴圈的初始化表示式,讓內迴圈的初始化表示式隨著外迴圈的i值變化
*
**
***
****
*****
/*
最多列印5行
最多列印5列
每一行和每一列關係是什麼? 列數<=行數
*/
for(int i = 0; i< 5; i++) {
    for(int j = 0; j <= i; j++) {
        printf("*");
    }
    printf("\n");
}
*****
****
***
**
*
for(int i = 0; i< 5; i++) {
    for(int j = i; j < 5; j++) {
        printf("*");
    }
    printf("\n");
}
  • 練習
    • 列印特殊三角形
1
12
123
for (int i = 0; i < 3; i++) {
    for (int j = 0; j <= i; j++) {
        printf("%d", j+1);
    }
    printf("\n");
}
  • 列印特殊三角形
1
22
333
for (int i = 1; i <= 3; i++) {
    for (int j = 1; j <= i; j++) {
        printf("%d", i);
    }
    printf("\n");
}
  • 列印特殊三角形
--*
-***
*****
for (int i = 0; i <= 5; i++) {
    for (int j = 0; j < 5 - i; j++) {
        printf("-");
    }
    for (int m = 0; m < 2*i+1; m++) {
        printf("*");
    }
    printf("\n");
}
  • 列印99乘法表
1 * 1 = 1
1 * 2 = 2     2 * 2 = 4
1 * 3 = 3     2 * 3 = 6     3 * 3 = 9
for (int i = 1; i <= 9; i++) {
    for (int j = 1; j <= i; j++) {
        printf("%d * %d = %d \t", j, i, (j * i));
    }
    printf("\n");
}

函數基本概念

  • C源程式是由函陣列成的
    • 例如: 我們前面學習的課程當中,通過main函數+scanf函數+printf函數+邏輯程式碼就可以組成一個C語言程式
  • C語言不僅提供了極為豐富的庫函數, 還允許使用者建立自己定義的函數。使用者可把自己的演演算法編寫成一個個相對獨立的函數,然後再需要的時候呼叫它
    • 例如:你用C語言編寫了一個MP3播放器程式,那麼它的程式結構如下圖所示
  • 可以說C程式的全部工作都是由各式各樣的函數完成的,所以也把C語言稱為函數式語言

函數的分類

  • 在C語言中可從不同的角度對函數分類
  • 從函數定義的角度看,函數可分為庫函數和使用者定義函數兩種
    • 庫函數: 由C語言系統提供,使用者無須定義,也不必在程式中作型別說明,只需在程式前包含有該函數原型的標頭檔案即可在程式中直接呼叫。在前面各章的例題中反覆用到printf、scanf、getchar、putchar等函數均屬此類
    • ***使用者定義函數:***由使用者按需編寫的函數。對於使用者自定義函數,不僅要在程式中定義函數本身,而且在主調函數模組中還必須對該被調函數進行型別說明,然後才能使用
  • 從函數執行結果的角度來看, 函數可分為有返回值函數和無返回值函數兩種
    • 有返回值函數: 此類函數被呼叫執行完後將向呼叫者返回一個執行結果,稱為函數返回值。(必須指定返回值型別和使用return關鍵字返回對應資料)
    • 無返回值函數: 此類函數用於完成某項特定的處理任務,執行完成後不向呼叫者返回函數值。(返回值型別為void, 不用使用return關鍵字返回對應資料)
  • 從主調函數和被調函數之間資料傳送的角度看,又可分為無參函數和有參函數兩種
    • 無參函數: 在函數定義及函數說明及函數呼叫中均不帶引數。主調函數和被調函數之間不進行引數傳送。
    • 有參函數: 在函數定義及函數說明時都有引數,稱為形式引數(簡稱為形參)。在函數呼叫時也必須給出引數,稱為實際引數(簡稱為實參)

函數的定義

  • 定義函數的目的

    • 將一個常用的功能封裝起來,方便以後呼叫
  • 自定義函數的書寫格式

返回值型別 函數名(引數型別 形式引數1,引數型別 形式引數2,…) {
    函數體;
    返回值;
}
  • 範例
int main(){
    printf("hello world\n");
    retrun 0;
}
  • 定義函數的步驟
    • 函數名:函數叫什麼名字
    • 函數體:函數是幹啥的,裡面包含了什麼程式碼
    • 返回值型別: 函數執行完畢返回什麼和呼叫者

  • 無參無返回值函數定義
    • 沒有返回值時return可以省略
    • 格式:
    void 函數名() {
        函數體;
    }
    
    • 範例:
    // 1.沒有返回值/沒有形參
    // 如果一個函數不需要返回任何資料給呼叫者, 那麼返回值型別就是void
    void printRose() {
        printf(" {@}\n");
        printf("  |\n");
        printf(" \\|/\n"); // 注意: \是一個特殊的符號(轉意字元), 想輸出\必須寫兩個斜線
        printf("  |\n");
      // 如果函數不需要返回資料給呼叫者, 那麼函數中的return可以不寫
    }
    

  • 無參有返回值函數定義
    • 格式:
    返回值型別 函數名() {
        函數體;
        return 值;
    }
    
    • 範例:
    int getMax() {
        printf("請輸入兩個整數, 以逗號隔開, 以回車結束\n");
        int number1, number2;
        scanf("%i,%i", &number1, &number2);
        int max = number1 > number2 ? number1 : number2;
        return max;
    }
    

  • 有參無返回值函數定義
    • 形式參數列列表的格式: 型別 變數名,型別 變數2,......
    • 格式:
    void 函數名(引數型別 形式引數1,引數型別 形式引數2,…) {
        函數體;
    }
    
    • 範例:
    void printMax(int value1, int value2) {
        int max = value1 > value2 ? value1 : value2;
        printf("max = %i\n", max);
    }
    

  • 有參有返回值函數定義
    • 格式:
    返回值型別 函數名(引數型別 形式引數1,引數型別 形式引數2,…) {
        函數體;
        return 0;
    }
    
    • 範例:
     int printMax(int value1, int value2) {
        int max = value1 > value2 ? value1 : value2;
        return max;
    }
    

  • 函數定義注意
    • 函數名稱不能相同
    void test() {
    }
    void test() { // 報錯
    }
    

函數的引數和返回值

  • 形式引數
    • 在***定義函數***時,函數名後面小括號()中定義的變數稱為形式引數,簡稱形參
    • 形參變數只有在被呼叫時才分配記憶體單元,在呼叫結束時,即刻釋放所分配的記憶體單元。
    • 因此,形參只有在函數內部有效,函數呼叫結束返回主調函數後則不能再使用該形參變數
int max(int number1, int number2) //  形式引數
{
    return number1 > number2 ? number1 : number2;
}

  • 實際引數
    • 在***呼叫函數***時, 傳入的值稱為實際引數,簡稱實參
    • 實參可以是常數、變數、表示式、函數等,無論實參是何種型別的量,在進行函數呼叫時,它們都必須具有確定的值,以便把這些值傳送給形參
    • 因此應預先用賦值,輸入等辦法使實參獲得確定值
int main() {
    int num = 99;
    // 88, num, 22+44均能得到一個確定的值, 所以都可以作為實參
    max(88, num, 22+44); // 實際引數
    return 0;
}

  • 形參、實參注意點
    • 呼叫函數時傳遞的實參個數必須和函數的形參個數必須保持一致
    int max(int number1, int number2) { //  形式引數
        return number1 > number2 ? number1 : number2;
    }
    int main() {
        // 函數需要2個形參, 但是我們只傳遞了一個實參, 所以報錯
        max(88); // 實際引數
        return 0;
    }
    
  • 形參實參型別不一致, 會自動轉換為形參型別
void change(double number1, double number2) {//  形式引數
   // 輸出結果: 10.000000, 20.000000
   // 自動將實參轉換為double型別後儲存
   printf("number1 = %f, number2 = %f", number1, number2);
}
int main() {
    change(10, 20);
    return 0;
}
  • 當使用基本資料型別(char、int、float等)作為實參時,實參和形參之間只是值傳遞,修改形參的值並不影響到實參函數可以沒有形參
    void change(int number1, int number2) { //  形式引數
        number1 = 250; // 不會影響實參
        number2 = 222;
    }
    int main() {
        int a = 88;
        int b = 99;
        change(a, b);
        printf("a  = %d, b = %d", a, b); // 輸出結果: 88, 99
        return 0;
    }
    

  • 返回值型別注意點
    • 如果沒有寫返回值型別,預設是int
    max(int number1, int number2) {//  形式引數
        return number1 > number2 ? number1 : number2;
    }
    
  • 函數返回值的型別和return實際返回的值型別應保持一致。如果兩者不一致,則以返回值型別為準,自動進行型別轉換
int height() {
    return 3.14; 
}
int main() {
  double temp = height();
  printf("%lf", temp);// 輸出結果: 3.000000
}
  • 一個函數內部可以多次使用return語句,但是return語句後面的程式碼就不再被執行
    int max(int number1, int number2) {//  形式引數
        return number1 > number2 ? number1 : number2;
        printf("執行不到"); // 執行不到
        return 250; // 執行不到
    }
    

函數的宣告

  • 在C語言中,函數的定義順序是有講究的:
    • 預設情況下,只有後面定義的函數才可以呼叫前面定義過的函數
  • 如果想把函數的定義寫在main函數後面,而且main函數能正常呼叫這些函數,那就必須在main函數的前面進行函數的宣告, 否則
    • 系統搞不清楚有沒有這個函數
    • 系統搞不清楚這個函數接收幾個引數
    • 系統搞不清楚這個函數的返回值型別是什麼
  • 所以函數宣告,就是在函數呼叫之前告訴系統, 該函數叫什麼名稱, 該函數接收幾個引數, 該函數的返回值型別是什麼
  • 函數的宣告格式:
    • 將自定義函數時{}之前的內容拷貝到呼叫之間即可
    • 例如: int max( int a, int b );
    • 或者: int max( int, int );
// 函數宣告
void getMax(int v1, int v2);
int main(int argc, const char * argv[]) {
    getMax(10, 20); // 呼叫函數
    return 0;
}
// 函數實現
void getMax(int v1, int v2) {
    int max = v1 > v2 ? v1 : v2;
    printf("max = %i\n", max);
}
  • 函數的宣告與實現的關係
    • 宣告僅僅代表著告訴系統一定有這個函數, 和這個函數的引數、返回值是什麼
    • 實現代表著告訴系統, 這個函數具體的業務邏輯是怎麼運作的
  • 函數宣告注意點:
    • 函數的實現不能重複, 而函數的宣告可以重複
    // 函數宣告
    void getMax(int v1, int v2);
    void getMax(int v1, int v2);
    void getMax(int v1, int v2); // 不會報錯
    int main(int argc, const char * argv[]) {
        getMax(10, 20); // 呼叫函數
        return 0;
    }
    // 函數實現
    void getMax(int v1, int v2) {
        int max = v1 > v2 ? v1 : v2;
        printf("max = %i\n", max);
    }
    
  • 函數宣告可以寫在函數外面,也可以寫在函數裡面, 只要在呼叫之前被宣告即可
    int main(int argc, const char * argv[]) {
        void getMax(int v1, int v2); // 函數宣告, 不會報錯
        getMax(10, 20); // 呼叫函數
        return 0;
    }
    // 函數實現
    void getMax(int v1, int v2) {
        int max = v1 > v2 ? v1 : v2;
        printf("max = %i\n", max);
    }
    
  • 當被調函數的函數定義出現在主調函數之前時,在主調函數中也可以不對被調函數再作宣告
// 函數實現
void getMax(int v1, int v2) {
    int max = v1 > v2 ? v1 : v2;
    printf("max = %i\n", max);
}
int main(int argc, const char * argv[]) {
    getMax(10, 20); // 呼叫函數
    return 0;
}
  • 如果被調函數的返回值是整型時,可以不對被調函數作說明,而直接呼叫
    int main(int argc, const char * argv[]) {
        int res = getMin(5, 3); // 不會報錯
        printf("result = %d\n", res );
        return 0;
    }
    int getMin(int num1, int num2) {// 返回int, 不用宣告
        return num1 < num2 ? num1 : num2;
    }
    

main函數分析

  • main的含義:
    • main是函數的名稱, 和我們自定義的函數名稱一樣, 也是一個識別符號
    • 只不過main這個名稱比較特殊, 程式已啟動就會自動呼叫它
  • return 0;的含義:
    • 告訴系統main函數是否正確的被執行了
    • 如果main函數的執行正常, 那麼就返回0
    • 如果main函數執行不正常, 那麼就返回一個非0的數
  • 返回值型別:
    • 一個函數return後面寫的是什麼型別, 函數的返回值型別就必須是什麼型別, 所以寫int
  • 形參列表的含義
    • int argc :
      • 系統在啟動程式時呼叫main函數時傳遞給argv的值的個數
    • const char * argv[] :
      • 系統在啟動程式時傳入的的值, 預設情況下系統只會傳入一個值, 這個值就是main函數執行檔案的路徑
      • 也可以通過命令列或專案設定傳入其它引數


  • 函數練習
    • 寫一個函數從鍵盤輸入三個整型數位,找出其最大值
    • 寫一個函數求三個數的平均值

遞迴函數(瞭解)

  • 什麼是遞迴函數?
    • 一個函數在它的函數體內呼叫它自身稱為遞迴呼叫
    void function(int x){
        function(x);
    }
    
  • 遞迴函數構成條件
    • 自己搞自己
    • 存在一個條件能夠讓遞迴結束
    • 問題的規模能夠縮小
  • 範例:
    • 獲取使用者輸入的數位, 直到使用者輸入一個正數為止
void getNumber(){
    int number = -1;
    while (number < 0) {
        printf("請輸入一個正數\n");
        scanf("%d", &number);
    }

    printf("number = %d\n", number);
}
void getNumber2(){
    int number = -1;
    printf("請輸入一個正數abc\n");
    scanf("%d", &number);
    if (number < 0) {
//        負數
        getNumber2();
    }else{
//        正數
       printf("number = %d\n", number);
    }
}
  • 遞迴和迴圈區別

    • 能用迴圈實現的功能,用遞迴都可以實現
    • 遞迴常用於"回溯", 「樹的遍歷」,"圖的搜尋"等問題
    • 但程式碼理解難度大,記憶體消耗大(易導致棧溢位), 所以考慮到程式碼理解難度和記憶體消耗問題, 在企業開發中一般能用迴圈都不會使用遞迴
  • 遞迴練習

    • 有5個人坐在一起,問第5個人多少歲?他說比第4個人大兩歲。問 第4個人歲數,他說比第3個人大兩歲。問第3個人,又說比第2個 人大兩歲。問第2個人,說比第1個人大兩歲。最後問第1個人, 他說是10歲。請問第5個人多大?
    • 用遞迴法求N的階乘
    • 設計一個函數用來計算B的n次方

進位制基本概念

  • 什麼是進位制?

    • 進位制是一種計數的方式,數值的表示形式
  • 常見的進位制

    • 十進位制、二進位制、八進位制、十六進位制
  • 進位制書寫的格式和規律

    • 十進位制 0、1、2、3、4、5、6、7、8、9 逢十進一
    • 二進位制 0、1 逢二進一
      • 書寫形式:需要以0b或者0B開頭,例如: 0b101
    • 八進位制 0、1、2、3、4、5、6、7 逢八進一
      • 書寫形式:在前面加個0,例如: 061
    • 十六進位制 0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F 逢十六進一
    • 書寫形式:在前面加個0x或者0X,例如: 0x45
  • 練習

    • 1.用不同進位製表示如下有多少個方格
    • 2.判斷下列數位是否合理
    00011  0x001  0x7h4  10.98  0986  .089-109
    +178  0b325  0b0010  0xffdc 96f 96.0f 96.oF  -.003
    

進位制轉換

  • 10 進位制轉 2 進位制
    • 除2取餘, 餘數倒序; 得到的序列就是二進位制表示形式
    • 例如: 將十進位制(97) 10轉換為二進位制數

  • 2 進位制轉 10 進位制
    • 每一位二進位制進位制位的值 * 2的當前索引次冪; 再將所有位求出的值相加
    • 例如: 將二進位制01100100轉換為十進位制
    01100100
    索引從右至左, 從零開始
    第0位: 0 * 2^0 = 0;
    第1位: 0 * 2^1 = 0;
    第2位: 1 * 2^2 = 4;
    第3位: 0 * 2^3 = 0;
    第4位元: 0 * 2^4 = 0;
    第5位: 1 * 2^5 = 32;
    第6位: 1 * 2^6 = 64;
    第7位: 0 * 2^7 = 0;
    最終結果為: 0 + 0 + 4 + 0 + 0 + 32 + 64 + 0 = 100
    

  • 2 進位制轉 8 進位制
    • 三個二進位制位代表一個八進位制位, 因為3個二進位制位的最大值是7,而八進位制是逢8進1
    • 例如: 將二進位制01100100轉換為八進位制數
    從右至左每3位劃分為8進位制的1位, 不夠前面補0
    001 100 100
    第0位: 100 等於十進位制 4
    第1位: 100 等於十進位制 4
    第2位: 001 等於十進位制 1
    最終結果: 144就是轉換為8進位制的值
    

  • 2 進位制轉 16 進位制
    • 四個二進位制位代表一個十六進位制位,因為4個二進位制位的最大值是15,而十六進位制是逢16進1
    • 例如: 將二進位制01100100轉換為十六進位制數
    從右至左每4位元劃分為16進位制的1位, 不夠前面補0
    0110 0100
    第0位: 0100 等於十進位制 4
    第1位: 0110 等於十進位制 6
    最終結果: 64就是轉換為16進位制的值
    

  • 其它進位制轉換為十進位制
    • 係數 * 基數 ^ 索引 之和
        十進位制           -->          十進位制
       12345   =  10000 + 2000 + 300 + 40 + 5
               =  (1 * 10 ^ 4)  + (2 * 10 ^ 3) + (3 * 10 ^ 2) + (4 * 10 ^ 1) + (5 * 10 ^ 0)
               =  (1 * 10000) + (2 + 1000) + (3 * 100) + (4 * 10) + (5 * 1)
               =  10000 + 2000 + 300 + 40 + 5
               =  12345
       
       規律:
       其它進位制轉換為十進位制的結果 = 係數 * 基數 ^ 索引 之和
       
       係數: 每一位的值就是一個係數 
       基數: 從x進位制轉換到十進位制, 那麼x就是基數
       索引: 從最低位以0開始, 遞增的數
    
       二進位制        -->      十進位制
       543210
       101101 = (1 * 2 ^ 5) + (0 * 2 ^ 4) + (1 * 2 ^ 3) + (1 * 2 ^ 2) + (0 * 2 ^ 1) + (1 * 2 ^ 0)
              = 32 + 0 + 8 + 4 + 0 + 1
              = 45
       
       八進位制        -->     十進位制
       016  =   (0 * 8 ^ 2) + (1 * 8 ^ 1) + (6 * 8 ^ 0)
            =    0  + 8 + 6
            =    14
       
       十六進位制      -->      十進位制
       0x11f =  (1 * 16 ^ 2) + (1 * 16 ^ 1) + (15 * 16 ^ 0)
             =   256  + 16 + 15
             =   287
    

  • 十進位制快速轉換為其它進位制
    • 十進位制除以基數取餘, 倒敘讀取
       十進位制        -->     二進位制
       100          -->    1100100
       100 / 2   = 50     0
       50  / 2   = 25     0
       25  / 2   = 12     1
       12  / 2   = 6      0
       6   / 2   = 3      0
       3   / 2   = 1      1
       1   / 2   = 0      1
       
       
       十進位制        -->     八進位制
       100          -->     144
       100 / 8    = 12    4
       12  / 8    = 1     4
       1   / 8    = 0     1
       
       十進位制        -->     十六進位制
       100          --> 64
       100 / 16   =  6    4
       6   / 16   =  0    6
    

十進位制小數轉換為二進位制小數

  • 整數部分,直接轉換為二進位制即可
  • 小數部分,使用"乘2取整,順序排列"
    • 用2乘十進位制小數,可以得到積,將積的整數部分取出,再用2乘餘下的小數部分,直到積中的小數部分為零,或者達到所要求的精度為止
    • 然後把取出的整數部分按順序排列起來, 即是小數部分二進位制
  • 最後將整數部分的二進位制和小數部分的二進位制合併起來, 即是一個二進位制小數
  • 例如: 將12.125轉換為二進位制
// 整數部分(除2取餘)
  12
/  2
------
   6    // 餘0
/  2
------
   3    // 餘0
/  2
------
   1   // 餘1
/  2
------
  0   // 餘1
//12 --> 1100
  
// 小數部分(乘2取整數積)
  0.125
*     2
  ------
   0.25  //0
   0.25
*     2
  ------
    0.5  //0
    0.5
*     2
  ------
    1.0  //1
    0.0
// 0.125 --> 0.001

// 12.8125 --> 1100.001

二進位制小數轉換為十進位制小數

  • 整數部分按照二進位制轉十進位制即可
  • 小數部分從最高位開始乘以2的負n次方, n從1開始
  • 例如: 將 1100.001轉換為十進位制
// 整數部分(乘以2的n次方, n從0開始)
0 * 2^0 = 0
0 * 2^1 = 0
1 * 2^2 = 4
1 * 2^3 = 8
 // 1100 == 8 + 4 + 0 + 0 == 12

// 小數部分(乘以2的負n次方, n從0開始)
0 * (1/2) = 0
0 * (1/4) = 0
1 * (1/8) = 0.125
// .100 == 0 + 0 + 0.125 == 0.125

// 1100.001  --> 12.125
  • 練習:
    • 將0.8125轉換為二進位制
    • 將0.1101轉換為十進位制
  0.8125
*      2
--------
   1.625  // 1
   0.625
*      2
--------
    1.25 // 1
    0.25
*      2
--------
     0.5 // 0
*      2
--------
    1.0 // 1
    0.0

// 0. 8125  --> 0.1101
1*(1/2) = 0.5
1*(1/4)=0.25
0*(1/8)=0
1*(1/16)=0.0625

//0.1101 --> 0.5 + 0.25 + 0 + 0.0625 == 0.8125

原碼反碼二補數

  • 計算機只能識別0和1, 所以計算機中儲存的資料都是以0和1的形式儲存的
  • 資料在計算機內部是以二補數的形式儲存的, 所有資料的運算都是以二補數進行的
  • 正數的原碼、反碼和二補數
    • 正數的原碼、反碼和二補數都是它的二進位制
    • 例如: 12的原碼、反碼和二補數分別為
      • 0000 0000 0000 0000 0000 0000 0000 1100
      • 0000 0000 0000 0000 0000 0000 0000 1100
      • 0000 0000 0000 0000 0000 0000 0000 1100
  • 負數的原碼、反碼和二補數
    • 二進位制的最高位我們稱之為符號位, 最高位是0代表是一個正數, 最高位是1代表是一個負數
    • 一個負數的原碼, 是將該負數的二進位制最高位變為1
    • 一個負數的反碼, 是將該數的原碼除了符號位以外的其它位取反
    • 一個負數的二補數, 就是它的反碼 + 1
    • 例如: -12的原碼、反碼和二補數分別為
      0000 0000 0000 0000 0000 0000 0000 1100 // 12二進位制
      1000 0000 0000 0000 0000 0000 0000 1100 // -12原碼
      1111 1111 1111 1111 1111 1111 1111 0011  // -12反碼
      1111 1111 1111 1111 1111 1111 1111 0100 // -12二補數
    
  • 負數的原碼、反碼和二補數逆向轉換
    • 反碼 = 二補數-1
    • 原碼= 反碼最高位不變, 其它位取反
      1111 1111 1111 1111 1111 1111 1111 0100 // -12二補數
      1111 1111 1111 1111 1111 1111 1111 0011  // -12反碼
      1000 0000 0000 0000 0000 0000 0000 1100 // -12原碼
    

  • 為什麼要引入反碼和二補數
    • 在學習本節內容之前,大家必須明白一個東西, 就是計算機只能做加法運算, 不能做減法和乘除法, 所以的減法和乘除法內部都是用加法來實現的
      • 例如: 1 - 1, 內部其實就是 1 + (-1);
      • 例如: 3 * 3, 內部其實就是 3 + 3 + 3;
      • 例如: 9 / 3, 內部其實就是 9 + (-3) + (-3) + (-3);
    • 首先我們先來觀察一下,如果只有原碼會儲存什麼問題
      • 很明顯, 通過我們的觀察, 如果只有原碼, 1-1的結果不對
        // 1 + 1
         0000 0000 0000 0000 0000 0000 0000 0001 // 1原碼
        +0000 0000 0000 0000 0000 0000 0000 0001 // 1原碼
         ---------------------------------------
         0000 0000 0000 0000 0000 0000 0000 0010  == 2
      
         // 1 - 1; 1 + (-1);
         0000 0000 0000 0000 0000 0000 0000 0001 // 1原碼
        +1000 0000 0000 0000 0000 0000 0000 0001 // -1原碼
         ---------------------------------------
         1000 0000 0000 0000 0000 0000 0000 0010 == -2
      
  • 正是因為對於減法來說,如果使用原碼結果是不正確的, 所以才引入了反碼
    • 通過反碼計算減法的結果, 得到的也是一個反碼;
    • 將計算的結果符號位不變其餘位取反,就得到了計算結果的原碼
    • 通過對原碼的轉換, 很明顯我們計算的結果是-0, 符合我們的預期
      // 1 - 1; 1 + (-1);
      0000 0000 0000 0000 0000 0000 0000 0001 // 1反碼
      1111 1111 1111 1111 1111 1111 1111 1110   // -1反碼
      ---------------------------------------
      1111 1111 1111 1111 1111 1111 1111 1111 // 計算結果反碼
      1000 0000 0000 0000 0000 0000 0000 0000 // 計算結果原碼 == -0
    
  • 雖然反碼能夠滿足我們的需求, 但是對於0來說, 前面的負號沒有任何意義, 所以才引入了二補數
    • 由於int只能儲存4個位元組, 也就是32位元資料, 而計算的結果又33位, 所以最高位溢位了,符號位變成了0, 所以最終得到的結果是0
      // 1 - 1; 1 + (-1);
      0000 0000 0000 0000 0000 0000 0000 0001 // 1二補數
      1111 1111 1111 1111 1111 1111 1111 1111   // -1二補數
      ---------------------------------------
     10000 0000 0000 0000 0000 0000 0000 0000 // 計算結果二補數
      0000 0000 0000 0000 0000 0000 0000 0000 //  == 0
    

位運運算元

  • 程式中的所有資料在計算機記憶體中都是以二進位制的形式儲存的。
  • 位運算就是直接對整數在記憶體中的二進位制位進行操作
  • C語言提供了6個位元運算運運算元, 這些運運算元只能用於整型運算元
符號名稱運算結果
&按位元與同1為1
|按位元或有1為1
^按位元互斥或不同為1
~按位元取反0變1,1變0
<<按位元左移乘以2的n次方
>>按位元右移除以2的n次方

  • 按位元與:
    • 只有對應的兩個二進位均為1時,結果位才為1,否則為0
    • 規律: 二進位制中,與1相&就保持原位,與0相&就為0
9&5 = 1

 1001
&0101
------
 0001

  • 按位元或:
    • 只要對應的二個二進位有一個為1時,結果位就為1,否則為0
9|5 = 13

 1001
|0101
------
 1101

  • 按位元互斥或
    • 當對應的二進位相異(不相同)時,結果為1,否則為0
    • 規律:
      • 相同整數相的結果是0。比如55=0
      • 多個整數相^的結果跟順序無關。例如: 567=576
      • 同一個數互斥或另外一個數兩次, 結果還是那個數。例如: 577 = 5
9^5 = 12

 1001
^0101
------
 1100

  • 按位元取反
    • 各二進位進行取反(0變1,1變0)
~9 =-10
0000 0000 0000 0000 0000 1001 // 取反前
1111 1111 1111 1111 1111 0110 // 取反後

// 根據負數二補數得出結果
1111 1111 1111 1111 1111 0110 // 二補數
1111 1111 1111 1111 1111 0101 // 反碼
1000 0000 0000 0000 0000 1010 // 原始碼 == -10

  • 位運算應用場景:
    • 判斷奇偶(按位元或)
       偶數: 的二進位制是以0結尾
       8   -> 1000
       10  -> 1010
       
       奇數: 的二進位制是以1結尾
       9   -> 1001
       11  -> 1011
    
       任何數和1進行&操作,得到這個數的最低位
       1000
      &0001
       -----
       0000  // 結果為0, 代表是偶數
    
       1011
      &0001
       -----
       0001 // 結果為1, 代表是奇數
    
    • 許可權系統
      enum Unix {
        S_IRUSR = 256,// 100000000 使用者可讀
        S_IWUSR = 128,//  10000000 使用者可寫
        S_IXUSR = 64,//    1000000 使用者可執行
        S_IRGRP = 32,//     100000 組可讀
        S_IWGRP = 16,//      10000 組可寫
        S_IXGRP = 8,//        1000 組可執行
        S_IROTH = 4,//         100 其它可讀
        S_IWOTH = 2,//          10 其它可寫
        S_IXOTH = 1 //           1 其它可執行
       };
    // 假設設定使用者許可權為可讀可寫
    printf("%d\n", S_IRUSR | S_IWUSR); // 384 // 110000000
    
    • 交換兩個數的值(按位元互斥或)
     a = a^b;
     b = b^a;
     a = a^b;
    

  • 按位元左移
    • 把整數a的各二進位全部左移n位,高位丟棄,低位補0
      • 由於左移是丟棄最高位,0補最低位,所以符號位也會被丟棄,左移出來的結果值可能會改變正負性
    • 規律: 左移n位其實就是乘以2的n次方
2<<1; //相當於 2 *= 2 // 4
  0010
<<0100

2<<2; //相當於 2 *= 2^2; // 8
  0010
<<1000
  • 按位元右移
    • 把整數a的各二進位全部右移n位,保持符號位不變
      • 為正數時, 符號位為0,最高位補0
      • 為負數時,符號位為1,最高位是補0或是補1(取決於編譯系統的規定)
    • 規律: 快速計算一個數除以2的n次方
2>>1; //相當於 2 /= 2 // 1
  0010
>>0001
4>>2; //相當於 4 /= 2^2 // 1
  0100
>>0001
  • 練習:
    • 寫一個函數把一個10進位制數按照二進位制格式輸出
#include <stdio.h>
void printBinary(int num);
int main(int argc, const char * argv[]) {
    printBinary(13);
}
void printBinary(int num){
    int len = sizeof(int)*8;
    int temp;
    for (int i=0; i<len; i++) {
        temp = num; //每次都在原數的基礎上進行移位運算
        temp = temp>>(31-i); //每次移動的位數
        int t = temp&1; //取出最後一位
        if(i!=0&&i%4==0)printf(" "); printf("%d",t);
    }
}

變數記憶體分析

  • 記憶體模型
    • 記憶體模型是線性的(有序的)
    • 對於 32 機而言,最大的記憶體地址是2^32次方bit(4294967296)(4GB)
    • 對於 64 機而言,最大的記憶體地址是2^64次方bit(18446744073709552000)(171億GB)

  • CPU 讀寫記憶體
    • CPU 在運作時要明確三件事
      • 儲存單元的地址(地址資訊)
      • 器件的選擇,讀 or 寫 (控制資訊)
      • 讀寫的資料 (資料資訊)
  • 如何明確這三件事情
    • 通過地址匯流排找到儲存單元的地址
    • 通過控制匯流排傳送記憶體讀寫指令
    • 通過資料匯流排傳輸需要讀寫的資料
  • 地址匯流排: 地址匯流排寬度決定了CPU可以存取的實體地址空間(定址能力)
    • 例如: 地址匯流排的寬度是1位, 那麼表示可以存取 0 和 1的記憶體
    • 例如: 地址匯流排的位數是2位, 那麼表示可以存取 00、01、10、11的記憶體
  • 資料匯流排: 資料匯流排的位數決定CPU單次通訊能交換的資訊數量
    • 例如: 資料匯流排:的寬度是1位, 那麼一次可以傳輸1位二進位制資料
    • 例如: 地址匯流排的位數是2位,那麼一次可以傳輸2位二進位制資料
  • 控制匯流排: 用來傳送各種控制訊號
  • 寫入流程

    • CPU 通過地址線將找到地址為 FFFFFFFB 的記憶體
    • CPU 通過控制線發出記憶體寫入命令,選中記憶體晶片,並通知它,要其寫入資料。
    • CPU 通過傳輸線將資料 8 送入記憶體 FFFFFFFB 單元中
  • 讀取流程

    • CPU 通過地址線將找到地址為 FFFFFFFB 的記憶體
    • CPU 通過控制線發出記憶體讀取命令,選中記憶體晶片,並通知它,將要從中讀取資料
    • 記憶體將 FFFFFFFB 號單元中的資料 8 通過傳輸線送入 CPU暫存器中
  • 變數的儲存原則

    • 先分配位元組地址大記憶體,然後分配位元組地址小的記憶體(記憶體定址是由大到小)
    • 變數的首地址,是變數所佔儲存空間位元組地址(最小的那個地址 )
    • 低位儲存在低地址位元組上,高位儲存在高地址位元組上
    10的二進位制: 0b00000000 00000000 00000000 00001010
               高位元組←                        →低位元組
    

char型別記憶體儲存細節

  • char型別基本概念
    • char是C語言中比較靈活的一種資料型別,稱為「字元型」
    • char型別變數佔1個位元組儲存空間,共8位元
    • 除單個字元以外, C語言的的跳脫字元也可以利用char型別儲存
字元意義
\b退格(BS)當前位置向後回退一個字元
\r回車(CR),將當前位置移至本行開頭
\n換行(LF),將當前位置移至下一行開頭
\t水平製表(HT),跳到下一個 TAB 位置
\0用於表示字串的結束標記
\代表一個反斜線字元 \
\"代表一個雙引號字元"
\’代表一個單引號字元’
  • char型資料儲存原理
    • 計算機只能識別0和1, 所以char型別儲存資料並不是儲存一個字元, 而是將字元轉換為0和1之後再儲存
    • 正是因為儲存字元型別時需要將字元轉換為0和1, 所以為了統一, 老美就定義了一個叫做ASCII表的東東
    • ASCII表中定義了每一個字元對應的整數
    char ch1 = 'a'; 
    printf("%i\n", ch1); // 97

    char ch2 = 97;
    printf("%c\n", ch2); // a
  • char型別注意點
    • char型別佔一個位元組, 一箇中文字元佔3位元組(unicode表),所有char不可以儲存中文
    char c = '我'; // 錯誤寫法
    
    • 除跳脫字元以外, 不支援多個字元
    char ch = 'ab'; // 錯誤寫法
    
    • char型別儲存字元時會先查詢對應的ASCII碼值, 儲存的是ASCII值, 所以字元6和數位6儲存的內容不同
    char ch1 = '6'; // 儲存的是ASCII碼 64
    char ch2 = 6; //  儲存的是數位 6
    
  • 練習
    • 定義一個函數, 實現輸入一個小寫字母,要求轉換成大寫輸出

型別說明符

  • 型別說明符基本概念
    • C語言提供了說明長度說明符號位的兩種型別說明符, 這兩種型別說明符一共有4個:
      • short 短整型 (說明長度)
      • long 長整型 (說明長度)
      • signed 有符號型 (說明符號位)
      • unsigned 無符號型 (說明符號位)
  • 這些說明符一般都是用來修飾int型別的,所以在使用時可以省略int
  • 這些說明符都屬於C語言關鍵字

short和long

  • short和long可以提供不同長度的整型數,也就是可以改變整型數的取值範圍。
    • 在64bit編譯器環境下,int佔用4個位元組(32bit),取值範圍是-2^31 ~ 2^31-1;
    • short佔用2個位元組(16bit),取值範圍是-2^15 ~ 2^15-1;
    • long佔用8個位元組(64bit),取值範圍是-2^63 ~ 2^63-1
  • 總結一下:在64位元編譯器環境下:
    • short佔2個位元組(16位元)
    • int佔4個位元組(32位元)
    • long佔8個位元組(64位元)。
    • 因此,如果使用的整數不是很大的話,可以使用short代替int,這樣的話,更節省記憶體開銷。
  • 世界上的編譯器林林總總,不同編譯器環境下,int、short、long的取值範圍和佔用的長度又是不一樣的。比如在16bit編譯器環境下,long只佔用4個位元組。不過幸運的是,ANSI \ ISO制定了以下規則:
    • short跟int至少為16位元(2位元組)
    • long至少為32位元(4位元組)
    • short的長度不能大於int,int的長度不能大於long
    • char一定為為8位元(1位元組),畢竟char是我們程式設計能用的最小資料型別
  • 可以連續使用2個long,也就是long long。一般來說,long long的範圍是不小於long的,比如在32bit編譯器環境下,long long佔用8個位元組,long佔用4個位元組。不過在64bit編譯器環境下,long long跟long是一樣的,都佔用8個位元組。
#include <stdio.h>

int main()
{
    // char佔1個位元組, char的取值範圍 -2^7~2^7
    char num = 129;
    printf("size = %i\n", sizeof(num)); // 1
    printf("num = %i\n", num); // -127
    // short int 佔2個位元組, short int的取值範圍 -2^15~2^15-1
    short int num1 = 32769;// -32767
    printf("size = %i\n", sizeof(num1)); // 2
    printf("num1 = %hi\n", num1);

    // int佔4個位元組, int的取值範圍 -2^31~2^31-1
    int num2 = 12345678901;
    printf("size = %i\n", sizeof(num2)); // 4
    printf("num2 = %i\n", num2);

    // long在32位元佔4個位元組, 在64位元佔8個位元組
    long int num3 = 12345678901;
    printf("size = %i\n", sizeof(num3)); // 4或8
    printf("num3 = %ld\n", num3);

    // long在32位元佔8個位元組, 在64位元佔8個位元組 -2^63~2^63-1
    long long int num4 = 12345678901;
    printf("size = %i\n", sizeof(num4)); // 8
    printf("num4 = %lld\n", num4);
    
    // 由於short/long/long long一般都是用於修飾int, 所以int可以省略
    short num5 = 123;
    printf("num5 = %lld\n", num5);
    long num6 = 123;
    printf("num6 = %lld\n", num6);
    long long num7 = 123;
    printf("num7 = %lld\n", num7);
    return 0;
}

signed和unsigned

  • 首先要明確的:signed int等價於signed,unsigned int等價於unsigned
  • signed和unsigned的區別就是它們的最高位是否要當做符號位,並不會像short和long那樣改變資料的長度,即所佔的位元組數。
    • signed:表示有符號,也就是說最高位要當做符號位。但是int的最高位本來就是符號位,因此signed和int是一樣的,signed等價於signed int,也等價於int。signed的取值範圍是-2^31 ~ 2^31 - 1
    • unsigned:表示無符號,也就是說最高位並不當做符號位,所以不包括負數。
    • 因此unsigned的取值範圍是:0000 0000 0000 0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 1111 1111 1111,也就是0 ~ 2^32 - 1
#include <stdio.h>

int main()
{
    // 1.預設情況下所有型別都是由符號的
    int num1 = 9;
    int num2 = -9;
    int num3 = 0;
    printf("num1 = %i\n", num1);
    printf("num2 = %i\n", num2);
    printf("num3 = %i\n", num3);

    // 2.signed用於明確說明, 當前儲存的資料可以是有符號的, 一般情況下很少使用
    signed int num4 = 9;
    signed int num5 = -9;
    signed int num6 = 0;
    printf("num4 = %i\n", num4);
    printf("num5 = %i\n", num5);
    printf("num6 = %i\n", num6);

    // signed也可以省略資料型別, 但是不推薦這樣編寫
    signed num7 = 9;
    printf("num7 = %i\n", num7);
   

    // 3.unsigned用於明確說明, 當前不能儲存有符號的值, 只能儲存0和正數
    // 應用場景: 儲存銀行存款,學生分數等不能是負數的情況
    unsigned int num8 = -9;
    unsigned int num9 = 0;
    unsigned int num10 = 9;
    // 注意: 不看怎麼存只看怎麼取
    printf("num8 = %u\n", num8);
    printf("num9 = %u\n", num9);
    printf("num10 = %u\n", num10);
    return 0;
}
  • 注意點:
    • 修飾符號的說明符可以和修飾長度的說明符混合使用
    • 相同型別的說明符不能混合使用
    signed short int num1 = 666;
    signed unsigned int num2 = 666; // 報錯

陣列的基本概念

  • 陣列,從字面上看,就是一組資料的意思,沒錯,陣列就是用來儲存一組資料的

    • 在C語言中,陣列屬於構造資料型別
  • 陣列的幾個名詞

    • 陣列:一組相同資料型別資料的有序的集合
    • 陣列元素: 構成陣列的每一個資料。
    • 陣列的下標: 陣列元素位置的索引(從0開始)
  • 陣列的應用場景

    • 一個int型別的變數能儲存一個人的年齡,如果想儲存整個班的年齡呢?
      • 第一種方法是定義很多個int型別的變數來儲存
      • 第二種方法是隻需要定義一個int型別的陣列來儲存
#include <stdio.h>

int main(int argc, const char * argv[]) {
    /*
    // 需求: 儲存2個人的分數
    int score1 = 99;
    int score2 = 60;
    
    // 需求: 儲存全班同學的分數(130人)
    int score3 = 78;
    int score4 = 68;
    ...
    int score130 = 88;
    */
    // 陣列: 如果需要儲存`一組``相同型別`的資料, 就可以定義一個陣列來儲存
    // 只要定義好一個陣列, 陣列內部會給每一塊小的儲存空間一個編號, 這個編號我們稱之為 索引, 索引從0開始
    // 1.定義一個可以儲存3個int型別的陣列
    int scores[3];
    
    // 2.通過陣列的下標往陣列中存放資料
    scores[0] = 998;
    scores[1] = 123;
    scores[2] = 567;
   
    // 3.通過陣列的下標從陣列中取出存放的資料
    printf("%i\n", scores[0]);
    printf("%i\n", scores[1]);
    printf("%i\n", scores[2]);
    return 0;
}

定義陣列

  • 元素型別 陣列名[元素個數];
// int 元素型別
// ages 陣列名稱
// [10] 元素個數
int ages[10];

初始化陣列

  • 定義的同時初始化
  • 指定元素個數,完全初始化
    • 其中在{ }中的各資料值即為各元素的初值,各值之間用逗號間隔
int ages[3] = {4, 6, 9};
  • 不指定元素個數,完全初始化
    • 根據大括號中的元素的個數來確定陣列的元素個數
int nums[] = {1,2,3,5,6};
  • 指定元素個數,部分初始化
    • 沒有顯式初始化的元素,那麼系統會自動將其初始化為0
int nums[10] = {1,2};
  • 指定元素個數,部分初始化
int nums[5] = {[4] = 3,[1] = 2};
  • 不指定元素個數,部分初始化
int nums[] = {[4] = 3};
  • 先定義後初始化
int nums[3];
nums[0] = 1;
nums[1] = 2;
nums[2] = 3;
  • 沒有初始化會怎樣?
    • 如果定義陣列後,沒有初始化,陣列中是有值的,是隨機的垃圾數,所以如果想要正確使用陣列應該要進行初始化。
int nums[5];
printf("%d\n", nums[0]);
printf("%d\n", nums[1]);
printf("%d\n", nums[2]);
printf("%d\n", nums[3]);
printf("%d\n", nums[4]);
輸出結果:
0
0
1606416312
0
1606416414
  • 注意點:
  • 使用陣列時不能超出陣列的索引範圍使用, 索引從0開始, 到元素個數-1結束
  • 使用陣列時不要隨意使用未初始化的元素, 有可能是一個隨機值
  • 對於陣列來說, 只能在定義的同時初始化多個值, 不能先定義再初始化多個值
int ages[3];
ages = {4, 6, 9}; // 報錯

陣列的使用

  • 通過下標(索引)存取:
// 找到下標為0的元素, 賦值為10
ages[0]=10;
// 取出下標為2的元素儲存的值
int a = ages[2];
printf("a = %d", a);

陣列的遍歷

  • 陣列的遍歷:遍歷的意思就是有序地檢視陣列的每一個元素
    int ages[4] = {19, 22, 33, 13};
    for (int i = 0; i < 4; i++) {
        printf("ages[%d] = %d\n", i, ages[i]);
    }

陣列長度計算方法

  • 因為陣列在記憶體中佔用的位元組數取決於其儲存的資料型別和資料的個數
    • 陣列所佔用儲存空間 = 一個元素所佔用儲存空間 * 元素個數(陣列長度)
  • 所以計算陣列長度可以使用如下方法
    陣列的長度 = 陣列佔用的總位元組數 / 陣列元素佔用的位元組數
    int ages[4] = {19, 22, 33, 13};
    int length =  sizeof(ages)/sizeof(int);
    printf("length = %d", length);
輸出結果: 4

練習

  • 正序輸出(遍歷)陣列
    int ages[4] = {19, 22, 33, 13};
    for (int i = 0; i < 4; i++) {
        printf("ages[%d] = %d\n", i, ages[i]);
    }
  • 逆序輸出(遍歷)陣列
    int ages[4] = {19, 22, 33, 13};
    for (int i = 3; i >=0; i--) {
        printf("ages[%d] = %d\n", i, ages[i]);
    }
  • 從鍵盤輸入陣列長度,構建一個陣列,然後再通過for迴圈從鍵 盤接收數位給陣列初始化。並使用for迴圈輸出檢視

陣列內部儲存細節

  • 儲存方式:

    • 1)記憶體定址從大到小, 從高地址開闢一塊連續沒有被使用的記憶體給陣列
    • 2)從分配的連續儲存空間中, 地址小的位置開始給每個元素分配空間
    • 3)從每個元素分配的儲存空間中, 地址最大的位置開始儲存資料
    • 4)用陣列名指向整個儲存空間最小的地址
  • 範例

#include <stdio.h>
int main()
{
    int num = 9;
    char cs[] = {'l','n','j'};
    printf("cs = %p\n", &cs);       // cs = 0060FEA9
    printf("cs[0] = %p\n", &cs[0]); // cs[0] = 0060FEA9
    printf("cs[1] = %p\n", &cs[1]); // cs[1] = 0060FEAA
    printf("cs[2] = %p\n", &cs[2]); // cs[2] = 0060FEAB

    int nums[] = {2, 6};
    printf("nums = %p\n", &nums);      // nums = 0060FEA0
    printf("nums[0] = %p\n", &nums[0]);// nums[0] = 0060FEA0
    printf("nums[1] = %p\n", &nums[1]);// nums[1] = 0060FEA4
    
    return 0;
}

  • 注意:字元在記憶體中是以對應ASCII碼值的二進位制形式儲存的,而非上述的形式。

陣列的越界問題

  • 陣列越界導致的問題
    • 約錯物件
    • 程式崩潰
    char cs1[2] = {1, 2};
    char cs2[3] = {3, 4, 5};
    cs2[3] = 88; // 注意:這句存取到了不屬於cs1的記憶體
    printf("cs1[0] = %d\n", cs1[0] );
輸出結果: 88

為什麼上述會輸出88, 自己按照"陣列內部儲存細節"畫圖腦補


陣列注意事項

  • 在定義陣列的時候[]裡面只能寫整型常數或者是返回整型常數的表示式
 int ages4['A'] = {19, 22, 33};
 printf("ages4[0] = %d\n", ages4[0]);

  int ages5[5 + 5] = {19, 22, 33};
  printf("ages5[0] = %d\n", ages5[0]);

  int ages5['A' + 5] = {19, 22, 33};
  printf("ages5[0] = %d\n", ages5[0]);
  • 錯誤寫法
// 沒有指定元素個數,錯誤
int a[];

// []中不能放變數
int number = 10;
int ages[number]; // 老版本的C語言規範不支援
printf("%d\n", ages[4]);

int number = 10;
int ages2[number] = {19, 22, 33} // 直接報錯

// 只能在定義陣列的時候進行一次性(全部賦值)的初始化
int ages3[5];
ages10 = {19, 22, 33};

// 一個長度為n的陣列,最大下標為n-1, 下標範圍:0~n-1
int ages4[4] = {19, 22, 33}
ages4[8]; // 陣列角標越界
  • 練習
    • 從鍵盤錄入當天出售BTC的價格並計算出售的BTC的總價和平均價(比如說一天出售了10個位元幣)

陣列和函數

  • 陣列可以作為函數的引數使用,陣列用作函數引數有兩種形式:
    • 一種是把陣列元素作為實參使用
    • 一種是把陣列名作為函數的形參和實參使用

陣列元素作為函數引數

  • 陣列的元素作為函數實參,與同型別的簡單變數作為實參一樣,如果是基本資料型別, 那麼形參的改變不影響實參
void change(int val)// int val = number
{
    val = 55;
}
int main(int argc, const char * argv[])
{
    int ages[3] = {1, 5, 8};
    printf("ages[0] = %d", ages[0]);// 1
    change(ages[0]);
    printf("ages[0] = %d", ages[0]);// 1
}
  • 用陣列元素作函數引數不要求形參也必須是陣列元素

陣列名作為函數引數

  • 在C語言中,陣列名除作為變數的識別符號之外,陣列名還代表了該陣列在記憶體中的起始地址,因此,當陣列名作函數引數時,實參與形參之間不是"值傳遞",而是"地址傳遞"
  • 實引陣列名將該陣列的起始地址傳遞給形引陣列,兩個陣列共用一段記憶體單元, 系統不再為形引陣列分配儲存單元
  • 既然兩個陣列共用一段記憶體單元, 所以形引陣列修改時,實引陣列也同時被修改了
void change2(int array[3])// int array = 0ffd1
{
    array[0] = 88;
}
int main(int argc, const char * argv[])
{
    int ages[3] = {1, 5, 8};
    printf("ages[0] = %d", ages[0]);// 1
    change(ages);
    printf("ages[0] = %d", ages[0]);// 88
}

陣列名作函數引數的注意點

  • 在函數形參表中,允許不給出形引陣列的長度
void change(int array[])
{
    array[0] = 88;
}
  • 形引陣列和實引陣列的型別必須一致,否則將引起錯誤。
void prtArray(double array[3]) // 錯誤寫法
{
    for (int i = 0; i < 3; i++) {
        printf("array[%d], %f", i, array[i]);
    }
}
int main(int argc, const char * argv[])
{
    int ages[3] = {1, 5, 8};
    prtArray(ages[0]);
}
  • 當陣列名作為函數引數時, 因為自動轉換為了指標型別,所以在函數中無法動態計算除陣列的元素個數
void printArray(int array[])
{
    printf("printArray size = %lu\n", sizeof(array)); // 8
    int length = sizeof(array)/ sizeof(int); // 2
    printf("length = %d", length);
}
  • 練習:
    • 設計一個函數int arrayMax(int a[], int count)找出陣列元素的最大值
    • 從鍵盤輸入3個0-9的數位,然後輸出0~9中哪些數位沒有出現過
    • 要求從鍵盤輸入6個0~9的數位,排序後輸出

計數排序(Counting Sort)

  • 計數排序是一個非基於比較的排序演演算法,該演演算法於1954年由 Harold H. Seward 提出。它的優勢在於在對一定範圍內的整數排序時,快於任何比較排序演演算法。

  • 排序思路:

    • 1.找出待排序陣列最大值
    • 2.定義一個索引最大值為待排序陣列最大值的陣列
    • 3.遍歷待排序陣列, 將待排序陣列遍歷到的值作新陣列索引
    • 4.在新陣列對應索引儲存值原有基礎上+1
  • 簡單程式碼實現:

int main()
{
    // 待排序陣列
    int nums[5] = {3, 1, 2, 0, 3};
    // 用於排序陣列
    int newNums[4] = {0};
    // 計算待排序陣列長度
    int len = sizeof(nums) / sizeof(nums[0]);
    // 遍歷待排序陣列
    for(int i = 0; i < len; i++){
        // 取出待排序陣列當前值
        int index = nums[i];
        // 將待排序陣列當前值作為排序陣列索引
        // 將用於排序陣列對應索引原有值+1
        newNums[index] = newNums[index] +1;
    }
    
    // 計算待排序陣列長度
    int len2 = sizeof(newNums) / sizeof(newNums[0]);
    // 輸出排序陣列索引, 就是排序之後結果
    for(int i = 0; i < len2; i++){
        for(int j = 0; j < newNums[i]; j++){
            printf("%i\n", i);
        }
    }
    /*
    // 計算待排序陣列長度
    int len2 = sizeof(newNums) / sizeof(newNums[0]);
    // 還原排序結果到待排序陣列
    for(int i = 0; i < len2; i++){
        int index = 0;
        for(int i = 0; i < len; i++){
            for(int j = 0; j < newNums[i]; j++){
                nums[index++] = i;
            }
        }
    }
    */
    return 0;
}

選擇排序

  • 選擇排序(Selection sort)是一種簡單直觀的排序演演算法。它的工作原理如下。首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小元素,然後放到排序序列末尾。以此類推,直到所有元素均排序完畢。

  • 排序思路:

    • 假設按照升序排序
    • 1.用第0個元素和後面所有元素依次比較
    • 2.判斷第0個元素是否大於當前被比較元素, 一旦小於就交換位置
    • 3.第0個元素和後續所有元素比較完成後, 第0個元素就是最小值
    • 4.排除第0個元素, 用第1個元素重複1~3操作, 比較完成後第1個元素就是倒數第二小的值
    • 以此類推, 直到當前元素沒有可比較的元素, 排序完成
  • 程式碼實現:


// 選擇排序
void selectSort(int numbers[], int length) {
    
    // 外迴圈為什麼要-1?
    // 最後一位不用比較, 也沒有下一位和它比較, 否則會出現錯誤存取
    for (int i = 0; i < length; i++) {
        for (int j = i; j < length - 1; j++) {
            // 1.用當前元素和後續所有元素比較
            if (numbers[i] < numbers[j + 1]) {
                //  2.一旦發現小於就交換位置
                swapEle(numbers, i, j + 1);
            }
        }
    }
}
// 交換兩個元素的值, i/j需要交換的索引
void swapEle(int array[], int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

氣泡排序

  • 氣泡排序(Bubble Sort)是一種簡單的排序演演算法。它重複 地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演演算法的名字由來是因為越小的元素會經由交換慢慢「浮」到數列的頂端。
  • 排序思路:
    • 假設按照升序排序
    • 1.從第0個元素開始, 每次都用相鄰兩個元素進行比較
    • 2.一旦發現後面一個元素小於前面一個元素就交換位置
    • 3.經過一輪比較之後最後一個元素就是最大值
    • 4.排除最後一個元素, 以此類推, 每次比較完成之後最大值都會出現再被比較所有元素的最後
    • 直到當前元素沒有可比較的元素, 排序完成
  • 程式碼實現:
// 氣泡排序
void bubbleSort(int numbers[], int length) {
    for (int i = 0; i < length; i++) {
        // -1防止`角標越界`: 存取到了不屬於自己的索引
        for (int j = 0; j < length - i - 1; j++) {
           //  1.用當前元素和相鄰元素比較
            if (numbers[j] < numbers[j + 1]) {
                //  2.一旦發現小於就交換位置
                swapEle(numbers, j, j + 1);
            }
        }
    }
}
// 交換兩個元素的值, i/j需要交換的索引
void swapEle(int array[], int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

插入排序

  • 插入排序(Insertion-Sort)的演演算法描述是一種簡單直觀的排序演演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。
  • 排序思路:
    • 假設按照升序排序
    • 1.從索引為1的元素開始向前比較, 一旦前面一個元素大於自己就讓前面的元素先後移動
    • 2.直到沒有可比較元素或者前面的元素小於自己的時候, 就將自己插入到當前空出來的位置
  • 程式碼實現:
int main()
{
    // 待排序陣列
    int nums[5] = {3, 1, 2, 0, 3};
    // 0.計算待排序陣列長度
    int len = sizeof(nums) / sizeof(nums[0]);

    //  1.從第一個元素開始依次取出所有用於比較元素
    for (int i = 1; i < len; i++)
    {
        // 2.取出用於比較元素
        int temp = nums[i];
        int j = i;
        while(j > 0){
            // 3.判斷元素是否小於前一個元素
            if(temp < nums[j - 1]){
                // 4.讓前一個元素向後移動一位
                nums[j] = nums[j - 1];
            }else{
                break;
            }
            j--;
        }
        // 5.將元素插入到空出來的位置
        nums[j] = temp;
    }
}
int main()
{
    // 待排序陣列
    int nums[5] = {3, 1, 2, 0, 3};
    // 0.計算待排序陣列長度
    int len = sizeof(nums) / sizeof(nums[0]);

    //  1.從第一個元素開始依次取出所有用於比較元素
    for (int i = 1; i < len; i++)
    {
        // 2.遍歷取出前面元素進行比較
        for(int j = i; j > 0; j--)
        {
            // 3.如果前面一個元素大於當前元素,就交換位置
            if(nums[j-1] > nums[j]){
                int temp = nums[j];
                nums[j] = nums[j - 1];
                nums[j - 1] = temp;
            }else{
                break;
            }
        }
    }
}

希爾排序

  • 1959年Shell發明,第一個突破O(n2)的排序演演算法,是簡單插入排序的改進版。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。
  • 排序思路:
    • 1.希爾排序可以理解為插入排序的升級版, 先將待排序陣列按照指定步長劃分為幾個小陣列
    • 2.利用插入排序對小陣列進行排序, 然後將幾個排序的小陣列重新合併為原始陣列
    • 3.重複上述操作, 直到步長為1時,再利用插入排序排序即可
  • 程式碼實現:
int main()
{
    // 待排序陣列
    int nums[5] = {3, 1, 2, 0, 3};
    // 0.計算待排序陣列長度
    int len = sizeof(nums) / sizeof(nums[0]);

// 2.計算步長
    int gap = len / 2;
    do{
        //  1.從第一個元素開始依次取出所有用於比較元素
        for (int i = gap; i < len; i++)
        {
            // 2.遍歷取出前面元素進行比較
            int j = i;
            while((j - gap) >= 0)
            {
                printf("%i > %i\n", nums[j - gap], nums[j]);
                // 3.如果前面一個元素大於當前元素,就交換位置
                if(nums[j - gap] > nums[j]){
                    int temp = nums[j];
                    nums[j] = nums[j - gap];
                    nums[j - gap] = temp;
                }else{
                    break;
                }
                j--;
            }
        }
        // 每個小陣列排序完成, 重新計算步長
        gap = gap / 2;
    }while(gap >= 1);
}

江哥提示:
對於初學者而言, 排序演演算法一次不易於學習太多, 咋們先來5個玩一玩, 後續繼續講解其它5個


折半查詢

  • 基本思路
  • 在有序表中,取中間元素作為比較物件,若給定值與中間元素的要查詢的數相等,則查詢成功;若給定值小於中間元素的要查詢的數,則在中間元素的左半區繼續查詢;
  • 若給定值大於中間元素的要查詢的數,則在中間元素的右半區繼續查詢。不斷重複上述查詢過 程,直到查詢成功,或所查詢的區域無資料元素,查詢失敗

  • 實現步驟

  • 在有序表中,取中間元素作為比較物件,若給定值與中間元素的要查詢的數相等,則查詢成功;

  • 若給定值小於中間元素的要查詢的數,則在中間元素的左半區繼續查詢;

  • 若給定值大於中間元素的要查詢的數,則在中間元素的右半區繼續查詢。

  • 不斷重複上述查詢過 程,直到查詢成功,或所查詢的區域無資料元素,查詢失敗。

  • 程式碼實現

int findKey(int values[], int length, int key) {
    // 定義一個變數記錄最小索引
    int min = 0;
    // 定義一個變數記錄最大索引
    int max = length - 1;
    // 定義一個變數記錄中間索引
    int mid = (min + max) * 0.5;
    
    while (min <= max) {
        // 如果mid對應的值 大於 key, 那麼max要變小
        if (values[mid] > key) {
            max = mid - 1;
            // 如果mid對應的值 小於 key, 那麼min要變
        }else if (values[mid] < key) {
            min = mid + 1;
        }else {
            return mid;
        }
        // 修改完min/max之後, 重新計算mid的值
        mid = (min + max) * 0.5;
    }
    return -1;
}

進位制轉換(查表法)

  • 實現思路:
    • 將二進位制、八進位制、十進位制、十六進位制所有可能的字元都存入陣列
    • 利用按位元與運運算元和右移依次取出當前進位制對應位置的值
    • 利用取出的值到陣列中查詢當前位輸出的結果
    • 將查詢的結果存入一個新的陣列, 當所有位都查詢儲存完畢, 新陣列中的值就是對應進位制的值
  • 程式碼實現
#include <stdio.h>
void toBinary(int num)
{
    total(num, 1, 1);
}
void toOct(int num)
{
    total(num, 7, 3);
}
void toHex(int num)
{
    total(num, 15, 4);
}

void total(int num , int base, int offset)
{
    //    1.定義表用於查詢結果
    char cs[] = {
        '0', '1', '2', '3', '4', '5',
        '6', '7', '8', '9', 'a', 'b',
        'c', 'd', 'e', 'f'
    };
    //    2.定義儲存結果的陣列
    char rs[32];
    //    計算最大的角標位置
    int length = sizeof(rs)/sizeof(char);
    int pos = length;//8

    while (num != 0) {
        int index = num & base;
        rs[--pos] = cs[index];
        num = num >> offset;
    }

    for (int i = pos; i < length; i++) {
        printf("%c", rs[i]);
    }
    printf("\n");
}
int main()
{
    toBinary(9);
    return 0;
}

二維陣列

  • 所謂二維陣列就是一個一維陣列的每個元素又被宣告為一 維陣列,從而構成二維陣列. 可以說二維陣列是特殊的一維陣列。
  • 範例:
    • int a[2][3] = { {80,75,92}, {61,65,71}};
    • 可以看作由一維陣列a[0]和一維陣列a[1]組成,這兩個一維陣列都包含了3個int型別的元素

二維陣列的定義

  • 格式:
    • 資料型別 陣列名[一維陣列的個數][一維陣列的元素個數]
    • 其中"一維陣列的個數"表示當前二維陣列中包含多少個一維陣列
    • 其中"一維陣列的元素個數"表示當前前二維陣列中每個一維陣列元素的個數

二維陣列的初始化

  • 二維數的初始化可分為兩種:

    • 定義的同時初始化
    • 先定義後初始化
  • 定義的同時初始化

int a[2][3]={ {80,75,92}, {61,65,71}};
  • 先定義後初始化
int a[2][3];
a[0][0] = 80;
a[0][1] = 75;
a[0][2] = 92;
a[1][0] = 61;
a[1][1] = 65;
a[1][2] = 71;
  • 按行分段賦值
int a[2][3]={ {80,75,92}, {61,65,71}};
  • 按行連續賦值
int a[2][3]={ 80,75,92,61,65,71};
  • 其它寫法
    • 完全初始化,可以省略第一維的長度
int a[][3]={{1,2,3},{4,5,6}};
int a[][3]={1,2,3,4,5,6};
  • 部分初始化,可以省略第一維的長度
int a[][3]={{1},{4,5}};
int a[][3]={1,2,3,4};
  • 注意: 有些人可能想不明白,為什麼可以省略行數,但不可以省略列數。也有人可能會問,可不可以只指定行數,但是省略列數?其實這個問題很簡單,如果我們這樣寫:
    int a[2][] = {1, 2, 3, 4, 5, 6}; // 錯誤寫法
    大家都知道,二維陣列會先存放第1行的元素,由於不確定列數,也就是不確定第1行要存放多少個元素,所以這裡會產生很多種情況,可能1、2是屬於第1行的,也可能1、2、3、4是第一行的,甚至1、2、3、4、5、6全部都是屬於第1行的
  • 指定元素的初始化
int a[2][3]={[1][2]=10};
int a[2][3]={[1]={1,2,3}}

二維陣列的應用場景



二維陣列的遍歷和儲存

二維陣列的遍歷

  • 二維陣列a[3][4],可分解為三個一維陣列,其陣列名分別為:
    • 這三個一維陣列都有4個元素,例如:一維陣列a[0]的 元素為a[0][0],a[0][1],a[0][2],a[0][3]。
    • 所以遍歷二維陣列無非就是先取出二維陣列中得一維陣列, 然後再從一維陣列中取出每個元素的值
  • 範例
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    printf("%c", cs[0][0]);// 第一個[0]取出一維陣列, 第二個[0]取出一維陣列中對應的元素
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    for (int i = 0; i < 2; i++) { // 外迴圈取出一維陣列
        // i
        for (int j = 0; j < 3; j++) {// 內迴圈取出一維陣列的每個元素
            printf("%c", cs[i][j]);
        }
        printf("\n");
    }

注意: 必須強調的是,a[0],a[1],a[2]不能當作下標變數使用,它們是陣列名,不是一個單純的下標變數


二維陣列的儲存

  • 和以為陣列一樣
    • 給陣列分配儲存空間從記憶體地址大開始分配
    • 給陣列元素分配空間, 從所佔用記憶體地址小的開始分配
    • 往每個元素中儲存資料從高地址開始儲存
#include <stdio.h>
int main()
{
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    // cs == &cs == &cs[0] == &cs[0][0]
    printf("cs = %p\n", cs);                // 0060FEAA
    printf("&cs = %p\n", &cs);              // 0060FEAA
    printf("&cs[0] = %p\n", &cs[0]);        // 0060FEAA
    printf("&cs[0][0] = %p\n", &cs[0][0]);  // 0060FEAA
    return 0;
}

二維陣列與函數

  • 值傳遞
#include <stdio.h>

// 和一位陣列一樣, 只看形參是基本型別還是陣列型別
// 如果是基本型別在函數中修改形參不會影響實參
void change(char ch){
    ch = 'n';
}
int main()
{
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    printf("cs[0][0] = %c\n", cs[0][0]); // a
    change(cs[0][0]);
    printf("cs[0][0] = %c\n", cs[0][0]); // a
    return 0;
}
  • 地址傳遞
#include <stdio.h>

// 和一位陣列一樣, 只看形參是基本型別還是陣列型別
// 如果是陣列型別在函數中修改形參會影響實參
void change(char ch[]){
    ch[0] = 'n';
}
int main()
{
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    printf("cs[0][0] = %c\n", cs[0][0]); // a
    change(cs[0]);
    printf("cs[0][0] = %c\n", cs[0][0]); // n
    return 0;
}
#include <stdio.h>

// 和一位陣列一樣, 只看形參是基本型別還是陣列型別
// 如果是陣列型別在函數中修改形參會影響實參
void change(char ch[][3]){
    ch[0][0] = 'n';
}
int main()
{
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    printf("cs[0][0] = %c\n", cs[0][0]); // a
    change(cs);
    printf("cs[0][0] = %c\n", cs[0][0]); // n
    return 0;
}

二維陣列作為函數引數注意點

  • 形參錯誤寫法
void test(char cs[2][]) // 錯誤寫法
{
    printf("我被執行了\n");
}

void test(char cs[2][3]) // 正確寫法
{
    printf("我被執行了\n");
}

void test(char cs[][3]) // 正確寫法
{
    printf("我被執行了\n");
}
  • 二維陣列作為函數引數,在被調函數中不能獲得其有多少行,需要通過引數傳入
void test(char cs[2][3])
{
    int row = sizeof(cs); // 輸出4或8
    printf("row = %zu\n", row);
}
  • 二維陣列作為函數引數,在被調函數中可以計算出二維陣列有多少列
void test(char cs[2][3])
{
    size_t col = sizeof(cs[0]); // 輸出3
    printf("col = %zd\n", col);
}

作業

  • 玩家通過鍵盤錄入 w,s,a,d控制小人向不同方向移動,其中w代表向上移動,s代表向 下移動,a代表向左移動,d 代表向右移動,當小人移動到出口位置,玩家勝利

  • 思路:

  • 1.定義二維陣列存放地圖

     ######
     #O #
     # ## #
     #  # #
     ##   #
     ######
  • 2.規定地圖的方向
  • 3.編寫程式控制方向
    • 當輸入w或者W, 小人向上移動. x-1
    • 當輸入s 或者S, 小人向下. x+1
    • 當輸入a或者A, 小人向左. y-1
    • 當輸入d或者D, 小人向右. y+1
  • 4.移動小人
    • 用變數記錄小人當前的位置
      • 1)如果小人將要移動的位置是牆,則無法移動
      • 2)如果小人將要移動的位置是路,則可以移動
  • 5.判斷是否走出迷宮

字串的基本概念

  • 字串是位於雙引號中的字元序列
    • 在記憶體中以「\0」結束,所佔位元組比實際多一個

字串的初始化

  • 在C語言中沒有專門的字串變數,通常用一個字元陣列來存放一個字串。
    • 當把一個字串存入一個陣列時,會把結束符‘\0’存入陣列,並以此作為該字串是否結束的標誌。
    • 有了‘\0’標誌後,就不必再用字元陣列 的長度來判斷字串的長度了
  • 初始化
    char name[9] = "lnj"; //在記憶體中以「\0」結束, \0ASCII碼值是0
    char name1[9] = {'l','n','j','\0'};
    char name2[9] = {'l','n','j',0};
    // 當陣列元素個數大於儲存字元內容時, 未被初始化的部分預設值是0, 所以下面也可以看做是一個字串
    char name3[9] = {'l','n','j'};
  • 錯誤的初始化方式
    //省略元素個數時, 不能省略末尾的\n
    // 不正確地寫法,結尾沒有\0 ,只是普通的字元陣列
    char name4[] = {'l','n','j'};

     //   "中間不能包含\0", 因為\0是字串的結束標誌
     //    \0的作用:字串結束的標誌
    char name[] = "c\0ool";
     printf("name = %s\n",name);
輸出結果: c

字串輸出

  • 如果字元陣列中儲存的是一個字串, 那麼字元陣列的輸入輸出將變得簡單方便。
    • 不必使用迴圈語句逐個地輸入輸出每個字元
    • 可以使用printf函數和scanf函數一次性輸出輸入一個字元陣列中的字串
  • 使用的格式字串為「%s」,表示輸入、輸出的是一個字串 字串的輸出

  • 輸出
    • %s的本質就是根據傳入的name的地址逐個去取陣列中的元素然後輸出,直到遇到\0位置
char chs[] = "lnj";
printf("%s\n", chs);
  • 注意點:
    • \0引發的髒讀問題
char name[] = {'c', 'o', 'o', 'l' , '\0'};
char name2[] = {'l', 'n', 'j'};
printf("name2 = %s\n", name2); // 輸出結果: lnjcool

  • 輸入
char ch[10];
scanf("%s",ch);
  • 注意點:
    • 對一個字串陣列, 如果不做初始化賦值, 必須指定陣列長度
    • ch最多存放由9個字元構成的字串,其中最後一個字元的位置要留給字串的結尾標示‘\0’
    • 當用scanf函數輸入字串時,字串中不能含有空格,否則將以空格作為串的結束符

字串常用方法

  • C語言中供了豐富的字串處理常式,大致可分為字串的輸入、輸出、合併、修改、比較、轉 換、複製、搜尋幾類。
    • 使用這些函數可大大減輕程式設計的負擔。
    • 使用輸入輸出的字串函數,在使用前應包含標頭檔案"stdio.h"
    • 使用其它字串函數則應包含標頭檔案"string.h"

  • 字串輸出函數:puts
    • 格式: puts(字元陣列名)
    • 功能:把字元陣列中的字串輸出到顯示器。即在螢幕上顯示該字串。
  • 優點:
    • 自動換行
    • 可以是陣列的任意元素地址
  • 缺點
    • 不能自定義輸出格式, 例如 puts(「hello %i」);
char ch[] = "lnj";
puts(ch); //輸出結果: lnj
  • puts函數完全可以由printf函數取代。當需要按一定格式輸出時,通常使用printf函數

  • 字串輸入函數:gets
    • 格式: gets (字元陣列名)
    • 功能:從標準輸入裝置鍵盤上輸入一個字串。
char ch[30];
gets(ch); // 輸入:lnj
puts(ch); // 輸出:lnj
  • 可以看出當輸入的字串中含有空格時,輸出仍為全部字串。說明gets函數並不以空格作為字串輸入結束的標誌,而只以回車作為輸入結束。這是與scanf函數不同的。
  • 注意gets很容易導致陣列下標越界,是一個不安全的字串操作函數

  • 字串長度
  • 利用sizeof字串長度
    • 因為字串在記憶體中是逐個字元儲存的,一個字元佔用一個位元組,所以字串的結束符長度也是佔用的記憶體單元的位元組數。
    char name[] = "it666";
    int size = sizeof(name);// 包含\0
    printf("size = %d\n", size); //輸出結果:6

  • 利用系統函數
    • 格式: strlen(字元陣列名)
    • 功能:測字串的實際長度(不含字串結束標誌‘\0’)並作為函數返回值。
    char name[] = "it666";
    size_t len = strlen(name2);
    printf("len = %lu\n", len); //輸出結果:5

  • 以「\0」為字串結束條件進行統計
/**
 *  自定義方法計算字串的長度
 *  @param name 需要計算的字串
 *  @return 不包含\0的長度
 */
int myStrlen2(char str[])
{
    //    1.定義變數儲存字串的長度
    int length = 0;
    while (str[length] != '\0')
    {
        length++;//1 2 3 4
    }
    return length;
}
/**
 *  自定義方法計算字串的長度
 *  @param name  需要計算的字串
 *  @param count 字串的總長度
 *  @return 不包含\0的長度
 */
int myStrlen(char str[], int count)
{
//    1.定義變數儲存字串的長度
    int length = 0;
//    2.通過遍歷取出字串中的所有字元逐個比較
    for (int i = 0; i < count; i++) {
//        3.判斷是否是字串結尾
        if (str[i] == '\0') {
            return length;
        }
        length++;
    }
    return length;
}

  • 字串連線函數:strcat
    • 格式: strcat(字元陣列名1,字元陣列名2)
    • 功能:把字元陣列2中的字串連線到字元陣列1 中字串的後面,並刪去字串1後的串標誌 「\0」。本函數返回值是字元陣列1的首地址。
char oldStr[100] = "welcome to";
char newStr[20] = " lnj";
strcat(oldStr, newStr);
puts(oldStr); //輸出: welcome to lnj"
  • 本程式把初始化賦值的字元陣列與動態賦值的字串連線起來。要注意的是,字元陣列1應定義足 夠的長度,否則不能全部裝入被連線的字串。

  • 字串拷貝函數:strcpy
    - 格式: strcpy(字元陣列名1,字元陣列名2)- 功能:把字元陣列2中的字串拷貝到字元陣列1中。串結束標誌「\0」也一同拷貝。字元數名2, 也可以是一個字串常數。這時相當於把一個字串賦予一個字元陣列。
char oldStr[100] = "welcome to";
char newStr[50] = " lnj";
strcpy(oldStr, newStr);
puts(oldStr); // 輸出結果:  lnj // 原有資料會被覆蓋
  • 本函數要求字元陣列1應有足夠的長度,否則不能全部裝入所拷貝的字串。

  • 字串比較函數:strcmp
    • 格式: strcmp(字元陣列名1,字元陣列名2)
    • 功能:按照ASCII碼順序比較兩個陣列中的字串,並由函數返回值返回比較結果。
      • 字串1=字串2,返回值=0;
      • 字串1>字串2,返回值>0;
      • 字串1<字串2,返回值<0。
    char oldStr[100] = "0";
    char newStr[50] = "1";
    printf("%d", strcmp(oldStr, newStr)); //輸出結果:-1
    char oldStr[100] = "1";
    char newStr[50] = "1";
    printf("%d", strcmp(oldStr, newStr));  //輸出結果:0
    char oldStr[100] = "1";
    char newStr[50] = "0";
    printf("%d", strcmp(oldStr, newStr)); //輸出結果:1

練習

  • 編寫一個函數char_contains(char str[],char key), 如果字串str中包含字元key則返回數值1,否則返回數值0

字串陣列基本概念

  • 字串陣列其實就是定義一個陣列儲存所有的字串
    • 1.一維字元陣列中存放一個字串,比如一個名字char name[20] = 「nj」
    • 2.如果要儲存多個字串,比如一個班所有學生的名字,則需要二維字元陣列,char names[15][20]可以存放15個學生的姓名(假設姓名不超過20字元)
    • 如果要儲存兩個班的學生姓名,那麼可以用三維字元陣列char names[2][15][20]
      ##字串陣列的初始化
char names[2][10] = { {'l','n','j','\0'}, {'l','y','h','\0'} };
char names2[2][10] = { {"lnj"}, {"lyh"} };
char names3[2][10] = { "lnj", "lyh" };

指標基本概念

  • 什麼是地址

    • 生活中的地址:
    • 記憶體地址:
  • 地址與記憶體單元中的資料是兩個完全不同的概念

    • 地址如同房間編號, 根據這個編號我們可以找到對應的房間
    • 記憶體單元如同房間, 房間是專門用於儲存資料的
  • 變數地址:

    • 系統分配給"變數"的"記憶體單元"的起始地址
int num = 6; // 佔用4個位元組
//那麼變數num的地址為: 0ff06

char c = 'a'; // 佔用1個位元組
//那麼變數c的地址為:0ff05


什麼是指標

  • 在計算機中所有資料都儲存在記憶體單元中,而每個記憶體單元都有一個對應的地址, 只要通過這個地址就能找到對應單元中儲存的資料.

  • 由於通過地址能找到所需的變數單元,所以我們說該地址指向了該變數單元。將地址形象化的稱為「指標」

  • 記憶體單元的指標(地址)和記憶體單元的內容是兩個不同的概念。

什麼是指標變數

  • 在C語言中,允許用一個變數來存放其它變數的地址, 這種專門用於儲存其它變數地址的變數, 我們稱之為指標變數
  • 範例:
    int age;// 定義一個普通變數
    num = 10;
    int *pnAge; // 定義一個指標變數
    pnAge = &age;

定義指標變數的格式

  • 指標變數的定義包括兩個內容:
    • 指標型別說明,即定義變數為一個指標變數;
    • 指標變數名;
  • 範例:
char ch = 'a';
char *p; // 一個用於指向字元型變數的指標
p = &ch;  
int num = 666;
int *q; // 一個用於指向整型變數的指標
q = &num;  
  • 其中,*表示這是一個指標變數
  • 變數名即為定義的指標變數名
  • 型別說明符表示本指標變數所指向的變數的資料型別

指標變數的初始化方法

  • 指標變數初始化的方法有兩種:定義的同時進行初始化和先定義後初始化
    • 定義的同時進行初始化
int a = 5;
int *p = &a;
  • 先定義後初始化
int a = 5;
int *p;
p=&a;
  • 把指標初始化為NULL
int *p=NULL;
int *q=0;
  • 不合法的初始化:
    • 指標變數只能儲存地址, 不能儲存其它型別
int *p;
p =  250; // 錯誤寫法
  • 給指標變數賦值時,指標變數前不能再加「*」
int *p;
*p=&a; //錯誤寫法
  • 注意點:

    • 多個指標變數可以指向同一個地址
  • 指標的指向是可以改變的

int a = 5;
int *p = &a;
int b = 10;
p = &b; // 修改指標指向
  • 指標沒有初始化裡面是一個垃圾值,這時候我們這是一個野指標
    • 野指標可能會導致程式崩潰
    • 野指標存取你不該存取資料
    • 所以指標必須初始化才可以存取其所指向儲存區域

存取指標所指向的儲存空間

  • C語言中提供了地址運運算元&來表示變數的地址。其一般形式為:
    • &變數名;
  • C語言中提供了*來定義指標變數和存取指標變數指向的記憶體儲存空間
    • 在定義變數的時候 * 是一個型別說明符,說明定義的這個變數是一個指標變數
int *p=NULL; // 定義指標變數
  • 在不是定義變數的時候 *是一個操作符,代表存取指標所指向儲存空間
int a = 5;
int *p = &a;
printf("a = %d", *p); // 存取指標變數

指標型別

  • 在同一種編譯器環境下,一個指標變數所佔用的記憶體空間是固定的。

  • 雖然在同一種編譯器下, 所有指標占用的記憶體空間是一樣的,但不同型別的變數卻佔不同的位元組數

    • 一個int佔用4個位元組,一個char佔用1個位元組,而一個double佔用8位元組;
    • 現在只有一個地址,我怎麼才能知道要從這個地址開始向後存取多少個位元組的儲存空間呢,是4個,是1個,還是8個。
    • 所以指標變數需要它所指向的資料型別告訴它要存取多少個位元組儲存空間

二級指標

  • 如果一個指標變數存放的又是另一個指標變數的地址,則稱這個指標變數為指向指標的指標變數。也稱為「二級指標」
    char c = 'a';
    char *cp;
    cp = &c;
    char **cp2;
    cp2 = &cp;
    printf("c = %c", **cp2);

  • 多級指標的取值規則
int ***m1;  //取值***m1
int *****m2; //取值*****m2

練習

  • 定義一個函數交換兩個變數的值
  • 寫一個函數,同時返回兩個數的和與差

##陣列指標的概念及定義

  • 陣列元素指標
    • 一個變數有地址,一個陣列包含若干元素,每個陣列元素也有相應的地址, 指標變數也可以儲存陣列元素的地址
    • 只要一個指標變數儲存了陣列元素的地址, 我們就稱之為陣列元素指標
    printf(%p %p」, &(a[0]), a); //輸出結果:0x1100, 0x1100
  • 注意: 陣列名a不代表整個陣列,只代表陣列首元素的地址。
  • 「p=a;」的作用是「把a陣列的首元素的地址賦給指標變數p」,而不是「把陣列a各元素的值賦給 p」

指標存取陣列元素

    int main (void)
{
      int a[5] = {2, 4, 6, 8, 22};
      int *p;
      // p = &(a[0]); 
      p = a;
      printf(%d %d\n」,a[0],*p); // 輸出結果: 2, 2
}

  • 在指標指向陣列元素時,允許以下運算:
    • 加一個整數(用+或+=),如p+1
    • 減一個整數(用-或-=),如p-1
    • 自加運算,如p++,++p
    • 自減運算,如p–,--p

  • 如果指標變數p已指向陣列中的一個元素,則p+1指向同一陣列中的下一個元素,p-1指向同 一陣列中的上一個元素。
  • 結論: 存取陣列元素,可用下面兩種方法:
    • 下標法, 如a[i]形式
    • 指標法, *(p+i)形式

  • 注意:
    • 陣列名雖然是陣列的首地址,但是陣列名所所儲存的陣列的首地址是不可以更改的
    int x[10];
	x++;  //錯誤
	int* p = x;
	p++; //正確

指標與字串

  • 定義字串的兩種方式
    • 字元陣列
char string[]=」I love lnj!」;
printf("%s\n",string);
    • 字串指標指向字串
// 陣列名儲存的是陣列第0個元素的地址, 指標也可以儲存第0個元素的地址
char *str = "abc"

  • 字串指標使用注意事項
    • 可以檢視字串的每一個字元
har *str = "lnj";
for(int i = 0; i < strlen(str);i++)
{
  printf("%c-", *(str+i)); // 輸出結果:l-n-j
}
    • 不可以修改字串內容
//   + 使用字元陣列來儲存的字串是儲存棧裡的,儲存棧裡面東西是可讀可寫,所有可以修改字串中的的字元
//   + 使用字元指標來儲存字串,它儲存的是字串常數地址,常數區是唯讀的,所以我們不可以修改字串中的字元
char *str = "lnj";
*(str+2) = 'y'; // 錯誤
    • 不能夠直接接收鍵盤輸入
// 錯誤的原因是:str是一個野指標,他並沒有指向某一塊記憶體空間
// 所以不允許這樣寫如果給str分配記憶體空間是可以這樣用 的
char *str;
scanf("%s", str);

指向函數指標

  • 為什麼指標可以指向一個函數?
    • 函數作為一段程式,在記憶體中也要佔據部分儲存空間,它也有一個起始地址
    • 函數有自己的地址,那就好辦了,我們的指標變數就是用來儲存地址的。
    • 因此可以利用一個指標指向一個函數。其中,函數名就代表著函數的地址。
  • 指標函數的定義
    • 格式: 返回值型別 (*指標變數名)(形參1, 形參2, ...);
    int sum(int a,int b)
    {
        return a + b;
    }

    int (*p)(int,int);
    p = sum;
  • 指標函數定義技巧

    • 1、把要指向函數頭拷貝過來
    • 2、把函數名稱使用小括號括起來
    • 3、在函數名稱前面加上一個*
    • 4、修改函數名稱
  • 應用場景

    • 呼叫函數
    • 將函數作為引數在函數間傳遞
  • 注意點:

    • 由於這類指標變數儲存的是一個函數的入口地址,所以對它們作加減運算(比如p++)是無意義的
    • 函數呼叫中"(指標變數名)"的兩邊的括號不可少,其中的不應該理解為求值運算,在此處它 只是一種表示符號

什麼是結構體

  • 結構體和陣列一樣屬於構造型別
  • 陣列是用於儲存一組相同型別資料的, 而結構體是用於儲存一組不同型別陣列的
    • 例如,在學生登記表中,姓名應為字元型;學號可為整型或字元型;年齡應為整型;性別應為字元型;成績可為整型或實型。
    • 顯然這組資料不能用陣列來存放, 為了解決這個問題,C語言中給出了另一種構造資料型別——「結構(structure)」或叫「結構體」。

定義結構體型別

  • 在使用結構體之前必須先定義結構體型別, 因為C語言不知道你的結構體中需要儲存哪些型別資料, 我們必須通過定義結構體型別來告訴C語言, 我們的結構體中需要儲存哪些型別的資料
  • 格式:
struct 結構體名{
     型別名1 成員名1;
     型別名2 成員名2;
     ……
     型別名n 成員名n;
 };
  • 範例:
struct Student {
    char *name; // 姓名
    int age; // 年齡
    float height; // 身高
};

定義結構體變數

  • 定好好結構體型別之後, 我們就可以利用我們定義的結構體型別來定義結構體變數

  • 格式: struct 結構體名 結構體變數名;

  • 先定義結構體型別,再定義變數

struct Student {
     char *name;
     int age;
 };

 struct Student stu;
  • 定義結構體型別的同時定義變數
struct Student {
    char *name;
    int age;
} stu;
  • 匿名結構體定義結構體變數
struct {
    char *name;
    int age;
} stu;
  • 第三種方法與第二種方法的區別在於,第三種方法中省去了結構體型別名稱,而直接給出結構變數,這種結構體最大的問題是結構體型別不能複用

結構體成員存取

  • 一般對結構體變數的操作是以成員為單位進行的,參照的一般形式為:結構體變數名.成員名
struct Student {
     char *name;
     int age;
 };
 struct Student stu;
 // 存取stu的age成員
 stu.age = 27;
 printf("age = %d", stu.age);

結構體變數的初始化

  • 定義的同時按順序初始化
struct Student {
     char *name;
     int age;
 };
struct Student stu = {「lnj", 27};
  • 定義的同時不按順序初始化
struct Student {
     char *name;
     int age;
 };
struct Student stu = {.age = 35, .name = 「lnj"};
  • 先定義後逐個初始化
struct Student {
     char *name;
     int age;
 };
 struct Student stu;
stu.name = "lnj";
stu.age = 35;
  • 先定義後一次性初始化
struct Student {
     char *name;
     int age;
 };
struct Student stu;
stu2 = (struct Student){"lnj", 35};

結構體型別作用域

  • 結構型別定義在函數內部的作用域與區域性變數的作用域是相同的
    • 從定義的那一行開始, 直到遇到return或者大括號結束為止
  • 結構型別定義在函數外部的作用域與全域性變數的作用域是相同的
    • 從定義的那一行開始,直到本檔案結束為止
//定義一個全域性結構體,作用域到檔案末尾
struct Person{
    int age;
    char *name;
};

int main(int argc, const char * argv[])
{
    //定義區域性結構體名為Person,會遮蔽全域性結構體
    //區域性結構體作用域,從定義開始到「}」塊結束
    struct Person{
        int age;
    };
    // 使用區域性結構體型別
    struct Person pp;
    pp.age = 50;
    pp.name = "zbz";

    test();
    return 0;
}

void test() {

    //使用全域性的結構體定義結構體變數p
    struct Person p = {10,"sb"};
    printf("%d,%s\n",p.age,p.name);
}

結構體陣列

  • 結構體陣列和普通陣列並無太大差異, 只不過是陣列中的元素都是結構體而已
  • 格式: struct 結構體型別名稱 陣列名稱[元素個數]
struct Student {
    char *name;
    int age;
};
struct Student stu[2]; 
  • 結構體陣列初始化和普通陣列也一樣, 分為先定義後初始化和定義同時初始化
    • 定義同時初始化
struct Student {
    char *name;
    int age;
};
struct Student stu[2] = {{"lnj", 35},{"zs", 18}}; 
    • 先定義後初始化
struct Student {
    char *name;
    int age;
};
struct Student stu[2]; 
stu[0] = {"lnj", 35};
stu[1] = {"zs", 18};

結構體指標

  • 一個指標變數當用來指向一個結構體變數時,稱之為結構體指標變數
  • 格式: struct 結構名 *結構指標變數名
  • 範例:
      // 定義一個結構體型別
      struct Student {
          char *name;
          int age;
      };

     // 定義一個結構體變數
     struct Student stu = {「lnj", 18};

     // 定義一個指向結構體的指標變數
     struct Student *p;

    // 指向結構體變數stu
    p = &stu;

     /*
      這時候可以用3種方式存取結構體的成員
      */
     // 方式1:結構體變數名.成員名
     printf("name=%s, age = %d \n", stu.name, stu.age);

     // 方式2:(*指標變數名).成員名
     printf("name=%s, age = %d \n", (*p).name, (*p).age);

     // 方式3:指標變數名->成員名
     printf("name=%s, age = %d \n", p->name, p->age);

     return 0;
 }
  • 通過結構體指標存取結構體成員, 可以通過以下兩種方式
    • (*結構指標變數).成員名
    • 結構指標變數->成員名(用熟)
  • (pstu)兩側的括號不可少,因為成員符「.」的優先順序高於「」。
  • 如去掉括號寫作pstu.num則等效於(pstu.num),這樣,意義就完全不對了。

結構體記憶體分析

  • 給結構體變數開闢儲存空間和給普通開闢儲存空間一樣, 會從記憶體地址大的位置開始開闢
  • 給結構體成員開闢儲存空間和給陣列元素開闢儲存空間一樣, 會從所佔用記憶體地址小的位置開始開闢
  • 結構體變數佔用的記憶體空間永遠是所有成員中佔用記憶體最大成員的倍數(對齊問題)

+多實際的計算機系統對基本型別資料在記憶體中存放的位置有限制,它們會要求這些資料的起始地址的值是 某個數k的倍數,這就是所謂的記憶體對齊,而這個k則被稱為該資料型別的對齊模數(alignment modulus)。

  • 這種強制的要求一來簡化了處理器與記憶體之間傳輸系統的設計,二來可以提升讀取資料的速度。比如這麼一種處理器,它每次讀寫記憶體的時候都從某個8倍數的地址開始,一次讀出或寫入8個位元組的資料,假如軟體能 保證double型別的資料都從8倍數地址開始,那麼讀或寫一個double型別資料就只需要一次記憶體操作。否則,我們就可能需要兩次記憶體操作才能完成這個動作,因為資料或許恰好橫跨在兩個符合對齊要求的8位元組 記憶體塊上

結構體變數佔用儲存空間大小

    struct Person{
        int age; // 4
        char ch; // 1
        double score; // 8
    };
    struct Person p;
    printf("sizeof = %i\n", sizeof(p)); // 16
  • 佔用記憶體最大屬性是score, 佔8個位元組, 所以第一次會分配8個位元組
  • 將第一次分配的8個位元組分配給age4個,分配給ch1個, 還剩下3個位元組
  • 當需要分配給score時, 發現只剩下3個位元組, 所以會再次開闢8個位元組儲存空間
  • 一共開闢了兩次8個位元組空間, 所以最終p佔用16個位元組
    struct Person{
        int age; // 4
        double score; // 8
        char ch; // 1
    };
    struct Person p;
    printf("sizeof = %i\n", sizeof(p)); // 24
  • 佔用記憶體最大屬性是score, 佔8個位元組, 所以第一次會分配8個位元組
  • 將第一次分配的8個位元組分配給age4個,還剩下4個位元組
  • 當需要分配給score時, 發現只剩下4個位元組, 所以會再次開闢8個位元組儲存空間
  • 將新分配的8個位元組分配給score, 還剩下0個位元組
  • 當需要分配給ch時, 發現上一次分配的已經沒有了, 所以會再次開闢8個位元組儲存空間
  • 一共開闢了3次8個位元組空間, 所以最終p佔用24個位元組

結構體巢狀定義

  • 成員也可以又是一個結構,即構成了巢狀的結構
struct Date{
     int month;
     int day;
     int year;
}
struct  stu{
     int num;
    char *name;
    char sex;
    struct Date birthday;
    Float score;
}
  • 在stu中巢狀儲存Date結構體內容
  • 注意:
  • 結構體不可以巢狀自己變數,可以巢狀指向自己這種型別的指標
struct Student {
    int age;
    struct Student stu;
};
  • 對巢狀結構體成員的存取
    • 如果某個成員也是結構體變數,可以連續使用成員運運算元"."存取最低一級成員
struct Date {
       int year;
       int month;
       int day;
  };

  struct Student {
      char *name;
      struct Date birthday;
 };

 struct Student stu;
 stu.birthday.year = 1986;
 stu.birthday.month = 9;
 stu.birthday.day = 10;

結構體和函數

  • 結構體雖然是構造型別, 但是結構體之間賦值是值拷貝, 而不是地址傳遞
    struct Person{
        char *name;
        int age;
    };
    struct Person p1 = {"lnj", 35};
    struct Person p2;
    p2 = p1;
    p2.name = "zs"; // 修改p2不會影響p1
    printf("p1.name = %s\n", p1.name); // lnj
    printf("p2.name = %s\n", p2.name); //  zs
  • 所以結構體變數作為函數形參時也是值傳遞, 在函數內修改形參, 不會影響外界實參
#include <stdio.h>

struct Person{
    char *name;
    int age;
};

void test(struct Person per);

int main()
{
    struct Person p1 = {"lnj", 35};
    printf("p1.name = %s\n", p1.name); // lnj
    test(p1);
    printf("p1.name = %s\n", p1.name); // lnj
    return 0;
}
void test(struct Person per){
    per.name = "zs";
}

共用體

  • 和結構體不同的是, 結構體的每個成員都是佔用一塊獨立的儲存空間, 而共用體所有的成員都佔用同一塊儲存空間
  • 和結構體一樣, 共用體在使用之前必須先定義共用體型別, 再定義共用體變數
  • 定義共用體型別格式:
union 共用體名{
    資料型別 屬性名稱;
    資料型別 屬性名稱;
    ...   ....
};
  • 定義共用體型別變數格式:
union 共用體名 共用體變數名稱;
  • 特點: 由於所有屬性共用同一塊記憶體空間, 所以只要其中一個屬性發生了改變, 其它的屬性都會受到影響
  • 範例:
    union Test{
        int age;
        char ch;
    };
    union Test t;
    printf("sizeof(p) = %i\n", sizeof(t));

    t.age = 33;
    printf("t.age = %i\n", t.age); // 33
    t.ch = 'a';
    printf("t.ch = %c\n", t.ch); // a
    printf("t.age = %i\n", t.age); // 97
  • 共用體的應用場景
    • (1)通訊中的封包會用到共用體,因為不知道對方會傳送什麼樣的封包過來,用共用體的話就簡單了,定義幾種格式的包,收到包之後就可以根據包的格式取出資料。
    • (2)節約記憶體。如果有2個很長的資料結構,但不會同時使用,比如一個表示老師,一個表示學生,要統計老師和學生的情況,用結構體就比較浪費記憶體,這時就可以考慮用共用體來設計。
      +(3)某些應用需要大量的臨時變數,這些變數型別不同,而且會隨時更換。而你的堆疊空間有限,不能同時分配那麼多臨時變數。這時可以使用共用體讓這些變數共用同一個記憶體空間,這些臨時變數不用長期儲存,用完即丟,和暫存器差不多,不用維護。

列舉

  • 什麼是列舉型別?

    • 在實際問題中,有些變數的取值被限定在一個有限的範圍內。例如,一個星期內只有七天,一年只有十二個月,一個班每週有六門課程等等。如果把這些量說明為整型,字元型或其它型別 顯然是不妥當的。
    • C語言提供了一種稱為「列舉」的型別。在「列舉」型別的定義中列舉出所有可能的取值, 被說明為該「列舉」型別的變數取值不能超過定義的範圍。
    • 該說明的是,列舉型別是一種基本資料型別,而不是一種構造型別,因為它不能再分解為任何基本型別。
  • 列舉型別的定義

    • 格式:
enum 列舉名 {
    列舉元素1,
    列舉元素2,
    ……
};
    • 範例:
// 表示一年四季
enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
};
  • 列舉變數
    • 先定義列舉型別,再定義列舉變數
enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
};
enum Season s;
    • 定義列舉型別的同時定義列舉變數
enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
} s;
  • 省略列舉名稱,直接定義列舉變數
enum {
    Spring,
    Summer,
    Autumn,
    Winter
} s;
  • 列舉型別變數的賦值和使用
enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
} s;
s = Spring; // 等價於 s = 0;
s = 3; // 等價於 s = winter;
printf("%d", s);
  • 列舉使用的注意
    • C語言編譯器會將列舉元素(spring、summer等)作為整型常數處理,稱為列舉常數。
    • 列舉元素的值取決於定義時各列舉元素排列的先後順序。預設情況下,第一個列舉元素的值為0,第二個為1,依次順序加1。
    • 也可以在定義列舉型別時改變列舉元素的值
enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
};
// 也就是說spring的值為0,summer的值為1,autumn的值為2,winter的值為3
enum Season {
    Spring = 9,
    Summer,
    Autumn,
    Winter
};
// 也就是說spring的值為9,summer的值為10,autumn的值為11,winter的值為12

全域性變數和區域性變數

  • 變數作用域基本概念
    • 變數作用域:變數的可用範圍
    • 按照作用域的不同,變數可以分為:區域性變數和全域性變數
  • 區域性變數
    • 定義在函數內部的變數以及函數的形參, 我們稱為區域性變數
    • 作用域:從定義的那一行開始, 直到遇到}結束或者遇到return為止
    • 生命週期: 從程式執行到定義哪一行開始分配儲存空間到程式離開該變數所在的作用域
    • 儲存位置: 區域性變數會儲存在記憶體的棧區中
    • 特點:
      • 相同作用域內不可以定義同名變數
      • 不同作用範圍可以定義同名變數,內部作用域的變數會覆蓋外部作用域的變數
  • 全域性變數
    • 定義在函數外面的變數稱為全域性變數
    • 作用域範圍:從定義哪行開始直到檔案結尾
    • 生命週期:程式一啟動就會分配儲存空間,直到程式結束
    • 儲存位置:靜態儲存區
    • 特點: 多個同名的全域性變數指向同一塊儲存空間

auto和register關鍵字

  • auto關鍵字(忘記)
    • 只能修飾區域性變數, 區域性變數如果沒有其它修飾符, 預設就是auto的
    • 特點: 隨用隨開, 用完即銷
auto int num; // 等價於 int num;
  • register關鍵字(忘記)
    • 只能修飾區域性變數, 原則上將記憶體中變數提升到CPU暫存器中儲存, 這樣存取速度會更快
    • 但是由於CPU暫存器數量相當有限, 通常不同平臺和編譯器在優化階段會自動轉換為auto
register int num; 

static關鍵字

  • 對區域性變數的作用
    • 延長區域性變數的生命週期,從程式啟動到程式退出,但是它並沒有改變變數的作用域
    • 定義變數的程式碼在整個程式執行期間僅僅會執行一次
#include <stdio.h>
void test();
int main()
{
    test();
    test();
    test();

    return 0;
}
void test(){
    static int num = 0; // 區域性變數
    num++; 
    // 如果不加static輸出 1 1 1
    // 如果新增static輸出 1 2 3
    printf("num = %i\n", num); 
}
  • 對全域性變數的作用
  • 全域性變數分類:
  • 內部變數:只能在本檔案中存取的變數
  • 外部變數:可以在其他檔案中存取的變數,預設所有全域性變數都是外部變數
  • 預設情況下多個同名的全域性變數共用一塊空間, 這樣會導致全域性變數汙染問題
  • 如果想讓某個全域性變數只在某個檔案中使用, 並且不和其他檔案中同名全域性變數共用同一塊儲存空間, 那麼就可以使用static
// A檔案中的程式碼
int num; // 和B檔案中的num共用
void test(){
    printf("ds.c中的 num = %i\n", num);
}
// B檔案中的程式碼
#include <stdio.h>
#include "ds.h"

int num; // 和A檔案中的num共用
int main()
{
    num = 666;
    test(); // test中輸出666
    return 0;
}
// A檔案中的程式碼
static int num; // 不和B檔案中的num共用
void test(){
    printf("ds.c中的 num = %i\n", num);
}
// B檔案中的程式碼
#include <stdio.h>
#include "ds.h"

int num; // 不和A檔案中的num共用
int main()
{
    num = 666;
    test(); // test中輸出0
    return 0;
}

extern關鍵字

  • 對區域性變數的作用
    • extern不能用於區域性變數
    • extern代表宣告一個變數, 而不是定義一個變數, 變數只有定義才會開闢儲存空間
    • 所以如果是區域性變數, 雖然提前宣告有某個區域性變數, 但是區域性變數只有執行到才會分配儲存空間
#include <stdio.h>

int main()
{
    extern int num;
    num = 998; // 使用時並沒有儲存空間可用, 所以宣告了也沒用
    int num; // 這裡才會開闢
    printf("num = %i\n", num);
    return 0;
}
  • 對全域性變數的作用
    • 宣告一個全域性變數, 代表告訴編譯器我在其它地方定義了這個變數, 你可以放心使用
#include <stdio.h>

int main()
{
    extern int num; // 宣告我們有名稱叫做num變數
    num = 998; // 使用時已經有對應的儲存空間
    printf("num = %i\n", num);
    return 0;
}
int num; // 全域性變數, 程式啟動就會分配儲存空間

static與extern對函數的作用

  • 內部函數:只能在本檔案中存取的函數

  • 外部函數:可以在本檔案中以及其他的檔案中存取的函數

  • 預設情況下所有的函數都是外部函數

  • static 作用

    • 宣告一個內部函數
static int sum(int num1,int num2);
  • 定義一個內部函數
static int sum(int num1,int num2)
{
  return num1 + num2;
}
  • extern作用
    • 宣告一個外部函數
extern int sum(int num1,int num2);
    • 定義一個外部函數
extern int sum(int num1,int num2)
{
  return num1 + num2;
}
  • 注意點:
  • 由於預設情況下所有的函數都是外部函數, 所以extern一般會省略
  • 如果只有函數宣告新增了static與extern, 而定義中沒有新增static與extern, 那麼無效

Qt Creator編譯過程做了什麼?

  • 當我們按下執行按鈕的時, 其實Qt Creator編譯器做了5件事情
    • 對原始檔進行預處理, 生成預處理檔案
    • 對預處理檔案進行編譯, 生成組合檔案
    • 對組合檔案進行編譯, 生成二進位制檔案
    • 對二進位制檔案進行連結, 生成可執行檔案
    • 執行可執行檔案

  • Qt Creator編譯過程驗證
    • 1.編寫程式碼, 儲存原始檔:
    #include <stdio.h>
    int main(){
        printf("hello lnj\n");
        return 0;
    }
    
  • 2.執行預處理編譯
  • 執行預處理編譯後生成的檔案
  • 開啟預處理編譯後生成的檔案
    • 處理原始檔中預處理相關的指令
    • 處理原始檔中多餘註釋等

  • 3.執行組合編譯
  • 執行組合編譯後生成的檔案
  • 開啟組合編譯後生成的檔案

  • 4.執行二進位制編譯
  • 執行二進位制編譯後生成的檔案
  • 開啟二進位制編譯後生成的檔案

  • 5.執行連結操作
    • 將依賴的一些C語言函數庫和我們編譯好的二進位制合併為一個檔案
  • 執行連結操作後生成的檔案

  • 6.執行連結後生成的檔案

計算機是運算過程分析

  • 1.編寫一個簡單的加法運算
  • 2.偵錯編寫好的程式碼, 檢視對應的組合檔案


  • 結論:
    • 1.通過地址線找到對應地址的儲存單元
    • 2.通過控制線傳送記憶體讀取指令
    • 3.通過傳輸線將記憶體中的值傳輸到CPU暫存器中
    • 4.在CPU中完成計算操作
    • 5.通過地址線找到對應地址的儲存單元
    • 6.通過控制線傳送記憶體寫入指令
    • 7.通過傳輸線將計算結果傳輸到記憶體中

預處理指令

預處理指令的概念

  • C語言在對源程式進行編譯之前,會先對一些特殊的預處理指令作解釋(比如之前使用的#include檔案包含指令),產生一個新的源程式(這個過程稱為編譯預處理),之後再進行通常的編譯
  • 為了區分預處理指令和一般的C語句,所有預處理指令都以符號「#」開頭,並且結尾不用分號
  • 預處理指令可以出現在程式的任何位置,它的作用範圍是從它出現的位置到檔案尾。習慣上我們儘可能將預處理指令寫在源程式開頭,這種情況下,它的作用範圍就是整個源程式檔案
  • C語言提供了多種預處理功能,如宏定義、檔案包含、條件編譯等。合理地使用預處理功能編寫的程式便於閱讀、修改、移植和偵錯,也有利於模組化程式設計。

宏定義

  • 被定義為「宏」的識別符號稱為「宏名」。在編譯預處理時,對程式中所有出現的「宏名」,都用宏定義中的字串去代換,這稱為「宏代換」或「宏展開」。
  • 宏定義是由源程式中的宏定義命令完成的。宏代換是由預處理程式自動完成的。在C語言中,「宏」分為有引數和無引數兩種。
    ##不帶引數的宏定義
  • 格式:#define 識別符號 字串
    • 其中的「#」表示這是一條預處理命令。凡是以「#」開頭的均為預處理命令。「define」為宏定義命令。「識別符號」為所定義的宏名。「字串」可以是常數、表示式、格式串等。
#include <stdio.h>

  // 源程式中所有的宏名PI在編譯預處理的時候都會被3.14所代替
  #define PI 3.14

 // 根據圓的半徑計radius算周長
 float girth(float radius) {
    return 2 * PI *radius;
}

int main ()
 {
    float g = girth(2);

    printf("周長為:%f", g);
    return 0;
}
  • 注意點:
  1. 宏名一般用大寫字母,以便與變數名區別開來,但用小寫也沒有語法錯誤
  • 2)對程式中用雙引號擴起來的字串內的字元,不進行宏的替換操作
#define R 10
 int main ()
 {
     char *s = "Radio"; // 在第1行定義了一個叫R的宏,但是第4行中"Radio"裡面的'R'並不會被替換成10

     return 0;
 }
  • 3)在編譯預處理用字串替換宏名時,不作語法檢查,只是簡單的字串替換。只有在編譯的時候才對已經展開宏名的源程式進行語法檢查
#define I 100
 int main ()
 {
     int i[3] = I;
     return 0;
 }
    1. 宏名的有效範圍是從定義位置到檔案結束。如果需要終止宏定義的作用域,可以用#undef命令
#define PI 3.14
int main ()
 {
    printf("%f", PI);
    return 0;
}
#undef PI
void test()
{
    printf("%f", PI); // 不能使用
}
    1. 定義一個宏時可以參照已經定義的宏名
#define R  3.0
#define PI 3.14
#define L  2*PI*R
#define S  PI*R*R
    1. 可用宏定義表示資料型別,使書寫方便
#define String char *
int main(int argc, const char * argv[])
{
     String str = "This is a string!";
     return 0;
}

帶引數的宏定義

  • C語言允許宏帶有引數。在宏定義中的引數稱為形式引數,在宏呼叫中的引數稱為實際引數。對帶引數的宏,在呼叫中,不僅要宏展開,而且要用實參去代換形參
  • 格式: #define 宏名(形參表) 字串
// 第1行中定義了一個帶有2個引數的宏average,
 #define average(a, b) (a+b)/2

int main ()
  {
  // 第4行其實會被替換成:int a = (10 + 4)/2;,
      int a = average(10, 4);
  // 輸出結果為:7是不是感覺這個宏有點像函數呢?
      printf("平均值:%d", a);
     return 0;
 }
  • 注意點:
  • 1)宏名和參數列之間不能有空格,否則空格後面的所有字串都作為替換的字串.
#define average (a, b) (a+b)/2

 int main ()
 {
     int a = average(10, 4);
     return 0;
 }
注意第1行的宏定義,宏名average跟(a, b)之間是有空格的,於是,第5行就變成了這樣:
int a = (a, b) (a+b)/2(10, 4);
這個肯定是編譯不通過的
  • 2)帶引數的宏在展開時,只作簡單的字元和引數的替換,不進行任何計算操作。所以在定義宏時,一般用一個小括號括住字串的引數。
#include <stdio.h>
  // 下面定義一個宏D(a),作用是返回a的2倍數值:
  #define D(a) 2*a
  // 如果定義宏的時候不用小括號括住引數

  int main ()
  {
  // 將被替換成int b = 2*3+4;,輸出結果10,如果定義宏的時候用小括號括住引數,把上面的第3行改成:#define D(a) 2*(a),注意右邊的a是有括號的,第7行將被替換成int b = 2*(3+4);,輸出結果14

     int b = D(3+4);
     printf("%d", b);
     return 0;
 }
  • 3)計算結果最好也用括號括起來
#include <stdio.h>
// 下面定義一個宏P(a),作用是返回a的平方
#define Pow(a) (a) * (a) // 如果不用小括號括住計算結果

int main(int argc, const char * argv[])      {
// 程式碼被替換為:int b = (10) * (10) / (2) * (2);
// 簡化之後:int b = 10 * (10 / 2) * 2;,最後變數b為:100
      int b = Pow(10) / Pow(2);

      printf("%d", b);
      return 0;
}
#include <stdio.h>
// 計算結果用括號括起來
#define Pow(a) ( (a) * (a) )

int main(int argc, const char * argv[])      {
// 程式碼被替換為:int b = ( (10) * (10) ) / ( (2) * (2) );
// 簡化之後:int b = (10 * 10) / (2 *2);,最後輸出結果:25
      int b = Pow(10) / Pow(2);

      printf("%d", b);
      return 0;
}

條件編譯

  • 在很多情況下,我們希望程式的其中一部分程式碼只有在滿足一定條件時才進行編譯,否則不參與編譯(只有參與編譯的程式碼最終才能被執行),這就是條件編譯。
  • 為什麼要使用條件編譯
    • 1)按不同的條件去編譯不同的程式部分,因而產生不同的目的碼檔案。有利於程式的移植和偵錯。
    • 2)條件編譯當然也可以用條件語句來實現。 但是用條件語句將會對整個源程式進行編譯,生成 的目的碼程式很長,而採用條件編譯,則根據條件只編譯其中的程式段1或程式段2,生成的目 標程式較短。
      ##if-#else 條件編譯指令
  • 第一種格式:
    • 它的功能是,如常數表示式的值為真(非0),則將code1 編譯到程式中,否則對code2編譯到程式中。
    • 注意:
      • 是將程式碼編譯進可執行程式, 而不是執行程式碼
      • 條件編譯後面的條件表示式中不能識別變數,它裡面只能識別常數和宏定義
#if 常數表示式
    ..code1...
#else
    ..code2...
#endif
#define SCORE 67
#if SCORE > 90
    printf("優秀\n");
#else
    printf("不及格\n");
#endif
  • 第二種格式:
#if 條件1
  ...code1...
 #elif 條件2
  ...code2...
 #else
  ...code3...
 #endif
#define SCORE 67
#if SCORE > 90
    printf("優秀\n");
#elif SCORE > 60
    printf("良好\n");
#else
    printf("不及格\n");
#endif

typedef關鍵字

  • C語言不僅􏰀供了豐富的資料型別,而且還允許由使用者自己定義型別說明符,也就是說允許由使用者為資料型別取「別名」。
  • 格式: typedef 原型別名 新型別名;
    • 其中原型別名中含有定義部分,新型別名一般用大寫表示,以便於區別。
    • 有時也可用宏定義來代替typedef的功能,但是宏定義是由預處理完成的,而typedef則是在編譯 時完成的,後者更為靈活方便。
      ##typedef使用
  • 基本資料型別
typedef int INTEGER
INTEGER a; // 等價於 int a;
  • 也可以在別名的基礎上再起一個別名
typedef int Integer;

typedef Integer MyInteger;

  • 用typedef定義陣列、指標、結構等型別將帶來很大的方便,不僅使程式書寫簡單而且使意義更為 明確,因而增強了可讀性。

  • 陣列型別

typedef char NAME[20]; // 表示NAME是字元陣列型別,陣列長度為20。然後可用NAME 說明變數,
NAME a; // 等價於 char a[20];
  • 結構體型別
    • 第一種形式:
 struct Person{
    int age;
    char *name;
};

typedef struct Person PersonType;
+ 第二種形式:
typedef struct Person{
    int age;
    char *name;
} PersonType;
+ 第三種形式:
typedef struct {
    int age;
    char *name;
} PersonType;
  • 列舉
    • 第一種形式:
enum Sex{
    SexMan,
    SexWoman,
    SexOther
};
typedef enum Sex SexType;
+ 第二種形式:
typedef enum Sex{
    SexMan,
    SexWoman,
    SexOther
} SexType;
+ 第三種形式:
typedef enum{
    SexMan,
    SexWoman,
    SexOther
} SexType;
  • 指標
    • typedef與指向結構體的指標
 // 定義一個結構體並起別名
  typedef struct {
      float x;
      float y;
  } Point;

 // 起別名
 typedef Point *PP;

  • typedef與指向函數的指標
// 定義一個sum函數,計算a跟b的和
  int sum(int a, int b) {
      int c = a + b;
      printf("%d + %d = %d", a, b, c);
      return c;
 }
 typedef int (*MySum)(int, int);

// 定義一個指向sum函數的指標變數p
 MySum p = sum;

宏定義與函數以及typedef區別

  • 與函數的區別
    • 從整個使用過程可以發現,帶引數的宏定義,在源程式中出現的形式與函數很像。但是兩者是有本質區別的:
      • 1> 宏定義不涉及儲存空間的分配、引數型別匹配、引數傳遞、返回值問題
      • 2> 函數呼叫在程式執行時執行,而宏替換隻在編譯預處理階段進行。所以帶引數的宏比函數具有更高的執行效率
  • typedef和#define的區別
    • 用宏定義表示資料型別和用typedef定義資料說明符的區別。
      • 宏定義只是簡單的字串替換,是在預處理完成的
      • typedef是在編譯時處理的,它不是作簡單的代換,而是對型別說明符重新命名。被命名的識別符號具有型別定義說明的功能
typedef char *String;
int main(int argc, const char * argv[])
{
     String str = "This is a string!";
     return 0;
}


#define String char *
int main(int argc, const char * argv[])
{
    String str = "This is a string!";
     return 0;
}
typedef char *String1; // 給char *起了個別名String1
#define String2 char * // 定義了宏String2
int main(int argc, const char * argv[]) {
        /*
        只有str1、str2、str3才是指向char型別的指標變數
        由於String1就是char *,所以上面的兩行程式碼等於:
        char *str1;
        char *str2;
        */
      String1 str1, str2;
        /*
        宏定義只是簡單替換, 所以相當於
        char *str3, str4;
        *號只對最近的一個有效, 所以相當於
        char *str3;
        char str4;
        */
      String2 str3, str4;
      return 0;
}

const關鍵字

  • const是一個型別修飾符
    • 使用const修飾變數則可以讓變數的值不能改變
      ##const有什麼主要的作用?
  • (1)可以定義const常數,具有不可變性
const int Max=100;
int Array[Max];
  • (2)便於進行型別檢查,使編譯器對處理內容有更多瞭解,消除了一些隱患。
 void f(const int i) { .........}
+ 編譯器就會知道i是一個常數,不允許修改;
  • (3)可以避免意義模糊的數位出現,同樣可以很方便地進行引數的調整和修改。 同宏定義一樣,可以做到不變則已,一變都變!如(1)中,如果想修改Max的內容,只需要:const int Max=you want;即可!

  • (4)可以保護被修飾的東西,防止意外的修改,增強程式的健壯性。 還是上面的例子,如果在 函數體內修改了i,編譯器就會報錯;

void f(const int i) { i=10;//error! }
  • (5) 可以節省空間,避免不必要的記憶體分配。
#define PI 3.14159 //常數宏
const doulbe Pi=3.14159; //此時並未將Pi放入ROM中 ...... double i=Pi; //此時為Pi分配記憶體,以後不再分配!
double I=PI; //編譯期間進行宏替換,分配記憶體
double j=Pi; //沒有記憶體分配
double J=PI; //再進行宏替換,又一次分配記憶體! const定義常數從組合的角度來看,只是給出了對應的記憶體地址,而不是象#define一樣給出的是立即數,所以,const定義的常數在程式執行過程中只有一份拷貝,而#define定義的常數在記憶體 中有若干個拷貝。
  • (6) 􏰀高了效率。編譯器通常不為普通const常數分配儲存空間,而是將它們儲存在符號表 中,這使得它成為一個編譯期間的常數,沒有了儲存與讀記憶體的操作,使得它的效率也很高。

如何使用const?

  • (1)修飾一般常數一般常數是指簡單型別的常數。這種常數在定義時,修飾符const可以用在型別說明符前,也可以用在型別說明符後
int const x=2;const int x=2;
  • (當然,我們可以偷樑換柱進行更新: 通過強制型別轉換,將地址賦給變數,再作修改即可以改變const常數值。)
    // const對於基本資料型別, 無論寫在左邊還是右邊, 變數中的值不能改變
    const int a = 5;
    // a = 666; // 直接修改會報錯
    // 偷樑換柱, 利用指標指向變數
    int *p;
    p = &a;
    // 利用指標間接修改變數中的值
    *p = 10;
    printf("%d\n", a); 
    printf("%d\n", *p);
  • (2)修飾常陣列(值不能夠再改變了)定義或說明一個常陣列可採用如下格式:
int const a[5]={1, 2, 3, 4, 5};
const int a[5]={1, 2, 3, 4, 5};
const int a[5]={1, 2, 3, 4, 5};
a[1] = 55; // 錯誤
  • (3)修飾函數的常引數const修飾符也可以修飾函數的傳遞引數,格式如下:void Fun(const int Var); 告訴編譯器Var在函數體中的無法改變,從而防止了使用者的一些無 意的或錯誤的修改。

  • (4)修飾函數的返回值: const修飾符也可以修飾函數的返回值,是返回值不可被改變,格式如 下:

const int Fun1();
const MyClass Fun2();
  • (5)修飾常指標

    • const int *A; //const修飾指標,A可變,A指向的值不能被修改
    • int const *A; //const修飾指向的物件,A可變,A指向的物件不可變
    • int *const A; //const修飾指標A, A不可變,A指向的物件可變
    • const int *const A;//指標A和A指向的物件都不可變
  • 技巧

 先看「*」的位置
 如果const 在 *的左側 表示值不能修改,但是指向可以改。
 如果const 在 *的右側 表示指向不能改,但是值可以改
 如果在「*」的兩側都有const 標識指向和值都不能改。

記憶體管理

程序空間

  • 程式,是經原始碼編譯後的可執行檔案,可執行檔案可以多次被執行,比如我們可以多次開啟 office。
  • 而程序,是程式載入到記憶體後開始執行,至執行結束,這樣一段時間概念,多次開啟的wps,每開啟一次都是一個程序,當我們每關閉一個 office,則表示該程序結束。
  • 程式是靜態概念,而程序動態/時間概念。
    ###程序空間圖示
    有了程序和程式的概念以後,我們再來看一下,程式被載入到記憶體以後記憶體空間佈局是什麼樣的

棧記憶體(Stack)

  • 棧中存放任意型別的變數,但必須是 auto 型別修飾的,即自動型別的區域性變數, 隨用隨開,用完即消。
  • 記憶體的分配和銷燬系統自動完成,不需要人工干預
  • 棧的最大尺寸固定,超出則引起棧溢位
    • 區域性變數過多,過大 或 遞迴層數太多等就會導致棧溢位
int ages[10240*10240]; // 程式會崩潰, 棧溢位
#include <stdio.h>

int main()
{
    // 儲存在棧中, 記憶體地址從大到小
    int a = 10;
    int b = 20;
    printf("&a = %p\n", &a); // &a = 0060FEAC
    printf("&b = %p\n", &b); // &b = 0060FEA8

    return 0;
}

堆記憶體(Heap)

  • 堆記憶體可以存放任意型別的資料,但需要自己申請與釋放
  • 堆大小,想像中的無窮大,但實際使用中,受限於實際記憶體的大小和記憶體是否連續性
int *p = (int *)malloc(10240 * 1024); // 不一定會崩潰
#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 儲存在棧中, 記憶體地址從小到大
    int *p1 = malloc(4);
    *p1 = 10;
    int *p2 = malloc(4);
    *p2 = 20;
   
    printf("p1 = %p\n", p1); //  p1 = 00762F48
    printf("p2 = %p\n", p2); // p2 = 00762F58

    return 0;
}

malloc函數

函數宣告void * malloc(size_t _Size);
所在檔案stdlib.h
函數功能申請堆記憶體空間並返回,所申請的空間並未初始化。
常見的初始化方法是memset 位元組初始化。
引數及返回解析
引數size_t _size 表示要申請的字元數
返回值void * 成功返回非空指標指向申請的空間 ,失敗返回 NULL
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    /*
     * malloc
     * 第一個引數: 需要申請多少個位元組空間
     * 返回值型別: void *
     */ 
    int *p = (int *)malloc(sizeof(int));
    printf("p = %i\n", *p); // 儲存垃圾資料
    /*
     * 第一個引數: 需要初始化的記憶體地址
     * 第二個初始: 需要初始化的值
     * 第三個引數: 需要初始化對少個位元組
     */ 
    memset(p, 0, sizeof(int)); // 對申請的記憶體空間進行初始化
    printf("p = %i\n", *p); // 初始化為0
    return 0;
}

free函數

  • 注意: 通過malloc申請的儲存空間一定要釋放, 所以malloc和free函數總是成對出現
函數宣告void free(void *p);
所在檔案stdlib.h
函數功能釋放申請的堆記憶體
引數及返回解析
引數void* p 指向手動申請的空間
返回值void 無返回
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    // 1.申請4個位元組儲存空間
    int *p = (int *)malloc(sizeof(int));
    // 2.初始化4個位元組儲存空間為0
    memset(p, 0, sizeof(int));
    // 3.釋放申請的儲存空間
    free(p);
    return 0;
}

calloc函數

函數宣告void *calloc(size_t nmemb, size_t size);
所在檔案stdlib.h
函數功能申請堆記憶體空間並返回,所申請的空間,自動清零
引數及返回解析
引數size_t nmemb 所需記憶體單元數量
引數size_t size 記憶體單元位元組數量
返回值void * 成功返回非空指標指向申請的空間 ,失敗返回 NULL
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    /*
    // 1.申請3塊4個位元組儲存空間
    int *p = (int *)malloc(sizeof(int) * 3);
    // 2.使用申請好的3塊儲存空間
    p[0] = 1;
    p[1] = 3;
    p[2] = 5;
    printf("p[0] = %i\n", p[0]);
    printf("p[1] = %i\n", p[1]);
    printf("p[2] = %i\n", p[2]);
    // 3.釋放空間
    free(p);
    */

    // 1.申請3塊4個位元組儲存空間
    int *p = calloc(3, sizeof(int));
    // 2.使用申請好的3塊儲存空間
    p[0] = 1;
    p[1] = 3;
    p[2] = 5;
    printf("p[0] = %i\n", p[0]);
    printf("p[1] = %i\n", p[1]);
    printf("p[2] = %i\n", p[2]);
    // 3.釋放空間
    free(p);

    return 0;
}

realloc函數

函數宣告void *realloc(void *ptr, size_t size);
所在檔案stdlib.h
函數功能擴容(縮小)原有記憶體的大小。通常用於擴容,縮小會會導致記憶體縮去的部分資料丟失。
引數及返回解析
引數void * ptr 表示待擴容(縮小)的指標, ptr 為之前用 malloc 或者 calloc 分配的記憶體地址。
引數size_t size 表示擴容(縮小)後記憶體的大小。
返回值void* 成功返回非空指標指向申請的空間 ,失敗返回 NULL。
  • 注意點:
    • 若引數ptr==NULL,則該函數等同於 malloc
    • 返回的指標,可能與 ptr 的值相同,也有可能不同。若相同,則說明在原空間後面申請,否則,則可能後續空間不足,重新申請的新的連續空間,原資料拷貝到新空間, 原有空間自動釋放
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    // 1.申請4個位元組儲存空間
    int *p = NULL;
    p = realloc(p, sizeof(int)); // 此時等同於malloc
    // 2.使用申請好的空間
    *p = 666;
    printf("*p = %i\n",  *p);
    // 3.釋放空間
    free(p);

    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    // 1.申請4個位元組儲存空間
    int *p = malloc(sizeof(int));
    printf("p = %p\n", p);
    // 如果能在傳入儲存空間地址後面擴容, 返回傳入儲存空間地址
    // 如果不能在傳入儲存空間地址後面擴容, 返回一個新的儲存空間地址
    p = realloc(p, sizeof(int) * 2);
    printf("p = %p\n", p);
    // 2.使用申請好的空間
    *p = 666;
    printf("*p = %i\n",  *p);
    // 3.釋放空間
    free(p);

    return 0;
}

連結串列

  • 連結串列實現了,記憶體零碎資料的有效組織。比如,當我們用 malloc 來進行記憶體申請的時候,當記憶體足夠,但是由於碎片太多,沒有連續記憶體時,只能以申請失敗而告終,而用連結串列這種資料結構來組織資料,就可以解決上類問題。

靜態連結串列

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

// 1.定義連結串列節點
typedef struct node{
    int data;
    struct node *next;
}Node;
int main()
{

    // 2.建立連結串列節點
    Node a;
    Node b;
    Node c;

    // 3.初始化節點資料
    a.data = 1;
    b.data = 3;
    c.data = 5;

    // 4.連結節點
    a.next = &b;
    b.next = &c;
    c.next = NULL;

    // 5.建立連結串列頭
    Node *head = &a;

    // 6.使用連結串列
    while(head != NULL){
        int currentData = head->data;
        printf("currentData = %i\n", currentData);
        head = head->next;
    }
    return 0;
}

動態連結串列

  • 靜態連結串列的意義不是很大,主要原因,資料儲存在棧上,棧的儲存空間有限,不能動態分配。所以連結串列要實現儲存的自由,要動態的申請堆裡的空間。

  • 有一個點要說清楚,我們的實現的連結串列是帶頭節點。至於,為什麼帶頭節點,需等大家對連結串列有個整體的的認知以後,再來體會,會更有意義。

  • 空連結串列

    • 頭指標帶了一個空連結串列節點, 空連結串列節點中的next指向NULL
#include <stdio.h>
#include <stdlib.h>

// 1.定義連結串列節點
typedef struct node{
    int data;
    struct node *next;
}Node;
int main()
{
    Node *head = createList();
    return 0;
}
// 建立空連結串列
Node *createList(){
    // 1.建立一個節點
    Node *node = (Node *)malloc(sizeof(Node));
    if(node == NULL){
        exit(-1);
    }
    // 2.設定下一個節點為NULL
    node->next = NULL;
    // 3.返回建立好的節點
    return node;
}
  • 非空連結串列
    • 頭指標帶了一個非空節點, 最後一個節點中的next指向NULL

動態連結串列頭插法

  • 1.讓新節點的下一個節點等於頭結點的下一個節點
  • 2.讓頭節點的下一個節點等於新節點
#include <stdio.h>
#include <stdlib.h>

// 1.定義連結串列節點
typedef struct node{
    int data;
    struct node *next;
}Node;
Node *createList();
void printNodeList(Node *node);
int main()
{
    Node *head = createList();
    printNodeList(head);
    return 0;
}
/**
 * @brief createList 建立連結串列
 * @return  建立好的連結串列
 */
Node *createList(){
    // 1.建立頭節點
    Node *head = (Node *)malloc(sizeof(Node));
    if(head == NULL){
        return NULL;
    }
    head->next = NULL;

    // 2.接收使用者輸入資料
    int num = -1;
    printf("請輸入節點資料\n");
    scanf("%i", &num);

    // 3.通過迴圈建立其它節點
    while(num != -1){
        // 3.1建立一個新的節點
        Node *cur = (Node *)malloc(sizeof(Node));
        cur->data = num;

        // 3.2讓新節點的下一個節點指向頭節點的下一個節點
        cur->next = head->next;
        // 3.3讓頭節點的下一個節點指向新節點
        head->next = cur;

        // 3.4再次接收使用者輸入資料
        scanf("%i", &num);
    }

    // 3.返回建立好的節點
    return head;
}
/**
 * @brief printNodeList 遍歷連結串列
 * @param node 連結串列指標頭
 */
void printNodeList(Node *node){
    Node *head = node->next;
    while(head != NULL){
        int currentData = head->data;
        printf("currentData = %i\n", currentData);
        head = head->next;
    }
}

動態連結串列尾插法

  • 1.定義變數記錄新節點的上一個節點
  • 2.將新節點新增到上一個節點後面
  • 3.讓新節點成為下一個節點的上一個節點
#include <stdio.h>
#include <stdlib.h>

// 1.定義連結串列節點
typedef struct node{
    int data;
    struct node *next;
}Node;
Node *createList();
void printNodeList(Node *node);
int main()
{
    Node *head = createList();
    printNodeList(head);
    return 0;
}
/**
 * @brief createList 建立連結串列
 * @return  建立好的連結串列
 */
Node *createList(){
    // 1.建立頭節點
    Node *head = (Node *)malloc(sizeof(Node));
    if(head == NULL){
        return NULL;
    }
    head->next = NULL;

    // 2.接收使用者輸入資料
    int num = -1;
    printf("請輸入節點資料\n");
    scanf("%i", &num);

    // 3.通過迴圈建立其它節點
    // 定義變數記錄上一個節點
    Node *pre = head;
    while(num != -1){
        // 3.1建立一個新的節點
        Node *cur = (Node *)malloc(sizeof(Node));
        cur->data = num;

        // 3.2讓新節點連結到上一個節點後面
        pre->next = cur;
        // 3.3當前節點下一個節點等於NULL
        cur->next = NULL;
        // 3.4讓當前節點程式設計下一個節點的上一個節點
        pre = cur;

        // 3.5再次接收使用者輸入資料
        scanf("%i", &num);
    }

    // 3.返回建立好的節點
    return head;
}
/**
 * @brief printNodeList 遍歷連結串列
 * @param node 連結串列指標頭
 */
void printNodeList(Node *node){
    Node *head = node->next;
    while(head != NULL){
        int currentData = head->data;
        printf("currentData = %i\n", currentData);
        head = head->next;
    }
}

動態鏈優化

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

// 1.定義連結串列節點
typedef struct node{
    int data;
    struct node *next;
}Node;
Node *createList();
void printNodeList(Node *node);
void insertNode1(Node *head, int data);
void insertNode2(Node *head, int data);
int main()
{
    // 1.建立一個空連結串列
    Node *head = createList();
    // 2.往空連結串列中插入資料
    insertNode1(head, 1);
    insertNode1(head, 3);
    insertNode1(head, 5);
    printNodeList(head);
    return 0;
}
/**
 * @brief createList 建立空連結串列
 * @return  建立好的空連結串列
 */
Node *createList(){
    // 1.建立頭節點
    Node *head = (Node *)malloc(sizeof(Node));
    if(head == NULL){
        return NULL;
    }
    head->next = NULL;
    // 3.返回建立好的節點
    return head;
}
/**
 * @brief insertNode1 尾插法插入節點
 * @param head 需要插入的頭指標
 * @param data 需要插入的資料
 * @return  插入之後的連結串列
 */
void insertNode1(Node *head, int data){
    // 1.定義變數記錄最後一個節點
    Node *pre = head;
    while(pre != NULL && pre->next != NULL){
        pre = pre->next;
    }
    // 2.建立一個新的節點
    Node *cur = (Node *)malloc(sizeof(Node));
    cur->data = data;

    // 3.讓新節點連結到上一個節點後面
    pre->next = cur;
    // 4.當前節點下一個節點等於NULL
    cur->next = NULL;
    // 5.讓當前節點程式設計下一個節點的上一個節點
    pre = cur;
}
/**
 * @brief insertNode1 頭插法插入節點
 * @param head 需要插入的頭指標
 * @param data 需要插入的資料
 * @return  插入之後的連結串列
 */
void insertNode2(Node *head, int data){
    // 1.建立一個新的節點
    Node *cur = (Node *)malloc(sizeof(Node));
    cur->data = data;

    // 2.讓新節點的下一個節點指向頭節點的下一個節點
    cur->next = head->next;
    // 3.讓頭節點的下一個節點指向新節點
    head->next = cur;
}
/**
 * @brief printNodeList 遍歷連結串列
 * @param node 連結串列指標頭
 */
void printNodeList(Node *node){
    Node *head = node->next;
    while(head != NULL){
        int currentData = head->data;
        printf("currentData = %i\n", currentData);
        head = head->next;
    }
}

連結串列銷燬

/**
 * @brief destroyList 銷燬連結串列
 * @param head 連結串列頭指標
 */
void destroyList(Node *head){
    Node *cur = NULL;
    while(head != NULL){
        cur = head->next;
        free(head);
        head = cur;
    }
}

連結串列長度計算

/**
 * @brief listLength 計算連結串列長度
 * @param head 連結串列頭指標
 * @return 連結串列長度
 */
int listLength(Node *head){
    int count = 0;
    head = head->next;
    while(head){
       count++;
       head = head->next;
    }
    return count;
}

連結串列查詢

/**
 * @brief searchList 查詢指定節點
 * @param head 連結串列頭指標
 * @param key 需要查詢的值
 * @return
 */
Node *searchList(Node *head, int key){
    head = head->next;
    while(head){
        if(head->data == key){
            break;
        }else{
            head = head->next;
        }
    }
    return head;
}

連結串列刪除

void deleteNodeList(Node *head, Node *find){
    while(head->next != find){
        head = head->next;
    }
    head->next = find->next;
    free(find);
}

作業

  • 給連結串列排序
/**
 * @brief bubbleSort 對連結串列進行排序
 * @param head 連結串列頭指標
 */
void bubbleSort(Node *head){
    // 1.計算連結串列長度
    int len = listLength(head);
    // 2.定義變數記錄前後節點
    Node *cur = NULL;
   // 3.相鄰元素進行比較, 進行氣泡排序
    for(int i = 0; i < len - 1; i++){
        cur = head->next;
        for(int j = 0; j < len - 1 - i; j++){
            printf("%i, %i\n", cur->data, cur->next->data);
            if((cur->data) > (cur->next->data)){
                int temp = cur->data;
                cur->data = cur->next->data;
                cur->next->data = temp;
            }
            cur = cur->next;
        }
    }
}
/**
 * @brief sortList 對連結串列進行排序
 * @param head 連結串列頭指標
 */
void sortList(Node *head){
    // 0.計算連結串列長度
    int len = listLength(head);
    // 1.定義變數儲存前後兩個節點
    Node *sh, *pre, *cur;
    for(int i = 0; i < len - 1; i ++){
        sh = head; // 頭節點
        pre = sh->next; // 第一個節點
        cur = pre->next; // 第二個節點
        for(int j = 0; j < len - 1 - i; j++){
            if(pre->data > cur->data){
                // 交換節點位置
                sh->next = cur;
                pre->next = cur->next;
                cur->next = pre;
                // 恢復節點名稱
                Node *temp = pre;
                pre = cur;
                cur = temp;
            }
            // 讓所有節點往後移動
            sh = sh->next;
            pre = pre->next;
            cur = cur->next;
        }
    }
}
  • 連結串列反轉
/**
 * @brief reverseList 反轉連結串列
 * @param head 連結串列頭指標
 */
void reverseList(Node *head){
    // 1.將連結串列一分為二
    Node *pre, *cur;
    pre = head->next;
    head->next = NULL;
    // 2.重新插入節點
    while(pre){
        cur = pre->next;
        pre->next = head->next;
        head->next = pre;

        pre = cur;
    }
}

檔案基本概念

  • 檔案流:
    • C 語言把檔案看作是一個字元的序列,即檔案是由一個一個字元組成的字元流,因此 c 語言將檔案也稱之為檔案流。
  • 檔案分類
    • 文字檔案

      • 以 ASCII 碼格式存放,一個位元組存放一個字元文字檔案的每一個位元組存放一個 ASCII 碼,代表一個字元。這便於對字元的逐個處理,但佔用儲存空間
        較多,而且要花費時間轉換。
      • .c檔案就是以文字檔案形式存放的
    • 二進位制檔案

      • 以二補數格式存放。二進位制檔案是把資料以二進位制數的格式存放在檔案中的,其佔用儲存空間較少。資料按其記憶體中的儲存形式原樣存放
      • .exe檔案就是以二進位制檔案形式存放的

  • 文字檔案和二進位制檔案範例
    • 下列程式碼暫時不要求看懂, 主要理解什麼是文字檔案什麼是二進位制檔案
#include <stdio.h>

int main()
{
    /*
     * 以文字形式儲存
     * 會將每個字元先轉換為對應的ASCII,
     * 然後再將ASCII碼的二進位制儲存到計算機中
     */
    int num = 666;
    FILE *fa = fopen("ascii.txt", "w");
    fprintf(fa, "%d", num);
    fclose(fa);

    /*
     * 以二進位制形式儲存
     * 會將666的二進位制直接儲存到檔案中
     */
    FILE *fb = fopen("bin.txt", "w");
    fwrite(&num, 4, 1, fb);
    fclose(fb);

    return 0;
}
  • 記憶體示意圖

  • 通過文字工具開啟示意圖

  • 文字工具預設會按照ASCII碼逐個直接解碼檔案, 由於文字檔案儲存的就是ASCII碼, 所以可以正常解析顯示, 由於二進位制檔案儲存的不是ASCII碼, 所以解析出來之後是亂碼

檔案的開啟和關閉

  • FILE 結構體
    • FILE 結構體是對緩衝區和檔案讀寫狀態的記錄者,所有對檔案的操作,都是通過FILE 結構體完成的。
  struct _iobuf {
    char *_ptr;  //檔案輸入的下一個位置
    int _cnt;  //當前緩衝區的相對位置
    char *_base; //檔案的起始位置)
    int _flag; //檔案標誌
    int _file;  //檔案的有效性驗證
    int _charbuf; //檢查緩衝區狀況,如果無緩衝區則不讀取
    int _bufsiz; // 緩衝區大小
    char *_tmpfname; //臨時檔名
  };
  typedef struct _iobuf FILE;

  • fileopen函數
函數宣告FILE * fopen ( const char * filename, const char * mode );
所在檔案stdio.h
函數功能以 mode 的方式,開啟一個 filename 命名的檔案,返回一個指向該檔案緩衝的 FILE 結構體指標。
引數及返回解析
引數char*filaname :要開啟,或是建立檔案的路徑。
引數char*mode :開啟檔案的方式。
返回值FILE* 返回指向檔案緩衝區的指標,該指標是後序操作檔案的控制程式碼。
mode處理方式當檔案不存在時當檔案存在時向檔案輸入從檔案輸出
r讀取出錯開啟檔案不能可以
w寫入建立新檔案覆蓋原有檔案可以不能
a追加建立新檔案在原有檔案後追加可以不能
r+讀取/寫入出錯開啟檔案可以可以
w+寫入/讀取建立新檔案覆蓋原有檔案可以可以
a+讀取/追加建立新檔案在原有檔案後追加可以可以

注意點:

  • Windows如果讀寫的是二進位制檔案,則還要加 b,比如 rb, r+b 等。 unix/linux 不區分文字和二進位制檔案

  • fclose函數
函數宣告int fclose ( FILE * stream );
所在檔案stdio.h
函數功能fclose()用來關閉先前 fopen()開啟的檔案.
函數功能此動作會讓緩衝區內的資料寫入檔案中, 並釋放系統所提供的檔案資源
引數及返回解析
引數FILE* stream :指向檔案緩衝的指標。
返回值int 成功返回 0 ,失敗返回 EOF(-1)。
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt", "w+");
    fclose(fp);
    return 0;
}

一次讀寫一個字元

  • 寫入
函數宣告int fputc (int ch, FILE * stream );
所在檔案stdio.h
函數功能將 ch 字元,寫入檔案。
引數及返回解析
引數FILE* stream :指向檔案緩衝的指標。
引數int : 需要寫入的字元。
返回值int 寫入成功,返回寫入成功字元,如果失敗,返回 EOF。
#include <stdio.h>

int main()
{
    // 1.開啟一個檔案
    FILE *fp = fopen("test.txt", "w+");

    // 2.往檔案中寫入內容
    for(char ch = 'a'; ch <= 'z'; ch++){
        // 一次寫入一個字元
        char res = fputc(ch, fp);
        printf("res = %c\n", res);
    }

    // 3.關閉開啟的檔案
    fclose(fp);
    return 0;
}
  • 讀取
函數宣告int fgetc ( FILE * stream );
所在檔案stdio.h
函數功能從檔案流中讀取一個字元並返回。
引數及返回解析
引數FILE* stream :指向檔案緩衝的指標。
返回值int 正常,返回讀取的字元;讀到檔案尾或出錯時,為 EOF。
#include <stdio.h>

int main()
{
    // 1.開啟一個檔案
    FILE *fp = fopen("test.txt", "r+");

    // 2.從檔案中讀取內容
    char res = EOF;
    while((res = fgetc(fp)) != EOF){
        printf("res = %c\n", res);
    }

    // 3.關閉開啟的檔案
    fclose(fp);
    return 0;
}
  • 判斷檔案末尾
    • feof函數
函數宣告int feof( FILE * stream );
所在檔案stdio.h
函數功能判斷檔案是否讀到檔案結尾
引數及返回解析
引數FILE* stream :指向檔案緩衝的指標。
返回值int 0 未讀到檔案結尾,非零 讀到檔案結尾。
#include <stdio.h>

int main()
{
    // 1.開啟一個檔案
    FILE *fp = fopen("test.txt", "r+");

    // 2.從檔案中讀取內容
    char res = EOF;
    // 注意: 由於只有先讀了才會修改標誌位,
    // 所以通過feof判斷是否到達檔案末尾, 一定要先讀再判斷, 不能先判斷再讀
    while((res = fgetc(fp)) && (!feof(fp))){
        printf("res = %c\n", res);
    }

    // 3.關閉開啟的檔案
    fclose(fp);
    return 0;
}
  • 注意點:
  • feof 這個函數,是去讀標誌位判斷檔案是否結束的。
  • 而標誌位只有讀完了才會被修改, 所以如果先判斷再讀標誌位會出現多打一次的的現象
  • 所以企業開發中使用feof函數一定要先讀後判斷, 而不能先判斷後讀
  • 作業
    • 實現檔案的簡單加密和解密
#include <stdio.h>
#include <string.h>
void encode(char *name, char *newName, int code);
void decode(char *name, char *newName, int code);
int main()
{
    encode("main.c", "encode.c", 666);
    decode("encode.c", "decode.c", 666);
    return 0;
}
/**
 * @brief encode 加密檔案
 * @param name 需要加密的檔名稱
 * @param newName 加密之後的檔名稱
 * @param code 祕鑰
 */
void encode(char *name, char *newName, int code){
    FILE *fw = fopen(newName, "w+");
    FILE *fr = fopen(name, "r+");
    char ch = EOF;
    while((ch = fgetc(fr)) && (!feof(fr))){
        fputc(ch ^ code, fw);
    }
    fclose(fw);
    fclose(fr);
}
/**
 * @brief encode 解密檔案
 * @param name 需要解密的檔名稱
 * @param newName 解密之後的檔名稱
 * @param code 祕鑰
 */
void decode(char *name, char *newName, int code){
    FILE *fw = fopen(newName, "w+");
    FILE *fr = fopen(name, "r+");
    char ch = EOF;
    while((ch = fgetc(fr)) && (!feof(fr))){
        fputc(ch ^ code, fw);
    }
    fclose(fw);
    fclose(fr);
}

一次讀寫一行字元

  • 什麼是行
  • 行是文字編輯器中的概念,檔案流中就是一個字元。這個在不同的平臺是有差異的。window 平臺 ‘\r\n’,linux 平臺是’\n’
  • 平臺差異
    • windows 平臺在寫入’\n’是會體現為’\r\n’,linux 平臺在寫入’\n’時會體現為’\n’。windows 平臺在讀入’\r\n’時,體現為一個字元’\n’,linux 平臺在讀入’\n’時,體現為一個字元’\n’
    • linux 讀 windows 中的換行,則會多讀一個字元,windows 讀 linux 中的換行,則沒有問題
#include <stdio.h>

int main()
{
    FILE *fw = fopen("test.txt", "w+");
    fputc('a', fw);
    fputc('\n', fw);
    fputc('b', fw);
    fclose(fw);
    return 0;
}


  • 寫入一行
函數宣告int fputs(char *str,FILE *fp)
所在檔案stdio.h
函數功能把 str 指向的字串寫入 fp 指向的檔案中。
引數及返回解析
引數char * str : 表示指向的字串的指標。
引數FILE *fp : 指向檔案流結構的指標。
返回值int 正常,返 0;出錯返 EOF。
#include <stdio.h>

int main()
{
    FILE *fw = fopen("test.txt", "w+");
    // 注意: fputs不會自動新增\n
    fputs("lnj\n", fw);
    fputs("it666\n", fw);
    fclose(fw);
    return 0;
}
  • 遇到\0自動終止寫入
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt", "w+");
    // 注意: fputs寫入時遇到\0就會自動終止寫入
    fputs("lnj\0it666\n", fp);

    fclose(fp);
    return 0;
}

  • 讀取一行
函數宣告char *fgets(char *str,int length,FILE *fp)
所在檔案stdio.h
函數功能從 fp 所指向的檔案中,至多讀 length-1 個字元,送入字元陣列 str 中, 如果在讀入 length-1 個字元結束前遇\n 或 EOF,讀入即結束,字串讀入後在最後加一個‘\0’字元。
引數及返回解析
引數char * str :指向需要讀入資料的緩衝區。
引數int length :每一次讀數位符的字數。
引數FILE* fp :檔案流指標。
返回值char * 正常,返 str 指標;出錯或遇到檔案結尾 返空指標 NULL。
  • 最多隻能讀取N-1個字元
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt", "w+");
    // 注意: fputs不會自動新增\n
    fputs("it666\n", fp);

    // 將FILE結構體中的讀寫指標重新移動到最前面
    // 注意: FILE結構體中讀寫指標每讀或寫一個字元后都會往後移動
    rewind(fp);
    char str[1024];
    // 從fp中讀取4個字元, 存入到str中
    // 最多隻能讀取N-1個字元, 會在最後自動新增\0
    fgets(str, 4, fp);

    printf("str = %s", str); // it6
    fclose(fp);
    return 0;
}
  • 遇到\n自動結束
#include <stdio.h>
int main()
{
    FILE *fp = fopen("test.txt", "w+");
    // 注意: fputs不會自動新增\n
    fputs("lnj\n", fp);
    fputs("it666\n", fp);

    // 將FILE結構體中的讀寫指標重新移動到最前面
    // 注意: FILE結構體中讀寫指標每讀或寫一個字元后都會往後移動
    rewind(fp);
    char str[1024];
    // 從fp中讀取1024個字元, 存入到str中
    // 但是讀到第4個就是\n了, 函數會自動停止讀取
    // 注意點: \n會被讀取進來
    fgets(str, 1024, fp);

    printf("str = %s", str); // lnj
    fclose(fp);
    return 0;
}
  • 讀取到EOF自動結束
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt", "w+");
    // 注意: fputs不會自動新增\n
    fputs("lnj\n", fp);
    fputs("it666", fp);

    // 將FILE結構體中的讀寫指標重新移動到最前面
    // 注意: FILE結構體中讀寫指標每讀或寫一個字元后都會往後移動
    rewind(fp);
    char str[1024];
    // 每次從fp中讀取1024個字元, 存入到str中
    // 讀取到檔案末尾自動結束
    while(fgets(str, 1024, fp)){
        printf("str = %s", str);
    }
    fclose(fp);
    return 0;
}
  • 注意點:
    • 企業開發中能不用feof函數就不用feof函數
    • 如果最後一行,沒有行‘\n’的話則少讀一行
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt", "w+");
    // 注意: fputs不會自動新增\n
    fputs("12345678910\n", fp);
    fputs("12345678910\n", fp);
    fputs("12345678910", fp);

    // 將FILE結構體中的讀寫指標重新移動到最前面
    // 注意: FILE結構體中讀寫指標每讀或寫一個字元后都會往後移動
    rewind(fp);
    char str[1024];
    // 每次從fp中讀取1024個字元, 存入到str中
    // 讀取到檔案末尾自動結束
    while(fgets(str, 1024, fp) && !feof(fp)){
        printf("str = %s", str);
    }
    fclose(fp);
    return 0;
}
  • 作業:
    • 利用fgets(str, 5, fp)讀取下列文字會讀取多少次?
12345678910
12345
123

一次讀寫一塊資料

  • C 語言己經從介面的層面區分了,文字的讀寫方式和二進位制的讀寫方式。前面我們講的是文字的讀寫方式。
  • 所有的檔案介面函數,要麼以 ‘\0’,表示輸入結束,要麼以 ‘\n’, EOF(0xFF)表示讀取結束。 ‘\0’ ‘\n’ 等都是文字檔案的重要標識,而所有的二進位制介面對於這些標識,是不敏感的。
    +二進位制的介面可以讀文字,而文字的介面不可以讀二進位制
  • 一次寫入一塊資料
函數宣告int fwrite(void *buffer, int num_bytes, int count, FILE *fp)
所在檔案stdio.h
函數功能把buffer 指向的資料寫入fp 指向的檔案中
引數char * buffer : 指向要寫入資料儲存區的首地址的指標
int num_bytes: 每個要寫的欄位的位元組數count
int count : 要寫的欄位的個數
FILE* fp : 要寫的檔案指標
返回值int 成功,返回寫的欄位數;出錯或檔案結束,返回 0。
#include <stdio.h>
#include <string.h>

int main()
{
    FILE *fp = fopen("test.txt", "wb+");
    // 注意: fwrite不會關心寫入資料的格式
    char *str = "lnj\0it666";
     /*
     * 第一個引數: 被寫入資料指標
     * 第二個引數: 每次寫入多少個位元組
     * 第三個引數: 需要寫入多少次
     * 第四個引數: 已開啟檔案結構體指標
     */
    fwrite((void *)str, 9, 1, fp);

    fclose(fp);
    return 0;
}
  • 一次讀取一塊資料
函數宣告int fread(void *buffer, int num_bytes, int count, FILE *fp)
所在檔案stdio.h
函數功能把fp 指向的檔案中的資料讀到 buffer 中。
引數char * buffer : 指向要讀入資料儲存區的首地址的指標
int num_bytes: 每個要讀的欄位的位元組數count
int count : 要讀的欄位的個數
FILE* fp : 要讀的檔案指標
返回值int 成功,返回讀的欄位數;出錯或檔案結束,返回 0。
#include <stdio.h>

int main()
{
    // test.txt中存放的是"lnj\0it666"
    FILE *fr = fopen("test.txt", "rb+");
    char buf[1024] = {0};
    // fread函數讀取成功返回讀取到的位元組數, 讀取失敗返回0
    /*
     * 第一個引數: 儲存讀取到資料的容器
     * 第二個引數: 每次讀取多少個位元組
     * 第三個引數: 需要讀取多少次
     * 第四個引數: 已開啟檔案結構體指標
     */ 
    int n = fread(buf, 1, 1024, fr);
    printf("%i\n", n);
    for(int i = 0; i < n; i++){
        printf("%c", buf[i]);
    }
    fclose(fr);
    return 0;
}
  • 注意點:
  • 讀取時num_bytes應該填寫讀取資料型別的最小單位, 而count可以隨意寫
  • 如果讀取時num_bytes不是讀取資料型別最小單位, 會引發讀取失敗
  • 例如: 儲存的是char型別 6C 6E 6A 00 69 74 36 36 36
    如果num_bytes等於1, count等於1024, 那麼依次取出 6C 6E 6A 00 69 74 36 36 36 , 直到取不到為止
    如果num_bytes等於4, count等於1024, 那麼依次取出[6C 6E 6A 00][69 74 36 36] , 但是最後還剩下一個36, 但又不滿足4個位元組, 那麼最後一個36則取不到
#include <stdio.h>
#include <string.h>

int main()
{

    // test.txt中存放的是"lnj\0it666"
    FILE *fr = fopen("test.txt", "rb+");
    char buf[1024] = {0};
    /*
    while(fread(buf, 4, 1, fr) > 0){
        printf("%c\n", buf[0]);
        printf("%c\n", buf[1]);
        printf("%c\n", buf[2]);
        printf("%c\n", buf[3]);
    }
    */
    /*
    while(fread(buf, 1, 4, fr) > 0){
        printf("%c\n", buf[0]);
        printf("%c\n", buf[1]);
        printf("%c\n", buf[2]);
        printf("%c\n", buf[3]);
    }
    */
    while(fread(buf, 1, 1, fr) > 0){
        printf("%c\n", buf[0]);
    }
    fclose(fr);
    return 0;
}
  • 注意: fwrite和fread本質是用來操作二進位制的
  • 所以下面用法才是它們的正確開啟姿勢
#include <stdio.h>

int main()
{

    FILE *fp = fopen("test.txt", "wb+");
    int ages[4] = {1, 3, 5, 6};
    fwrite(ages, sizeof(ages), 1, fp);
    rewind(fp);
    int data;
    while(fread(&data, sizeof(int), 1, fp) > 0){
        printf("data = %i\n", data);
    }
    return 0;
}

讀寫結構體

  • 結構體中的資料型別不統一,此時最適合用二進位制的方式進行讀寫
  • 讀寫單個結構體
#include <stdio.h>

typedef struct{
    char *name;
    int age;
    double height;
} Person;

int main()
{
    Person p1 = {"lnj", 35, 1.88};
//    printf("name = %s\n", p1.name);
//    printf("age = %i\n", p1.age);
//    printf("height = %lf\n", p1.height);

    FILE *fp = fopen("person.stu", "wb+");
    fwrite(&p1, sizeof(p1), 1, fp);

    rewind(fp);
    Person p2;
    fread(&p2, sizeof(p2), 1, fp);
    printf("name = %s\n", p2.name);
    printf("age = %i\n", p2.age);
    printf("height = %lf\n", p2.height);

    return 0;
}
  • 讀寫結構體陣列
#include <stdio.h>

typedef struct{
    char *name;
    int age;
    double height;
} Person;

int main()
{
    Person ps[] = {
      {"zs", 18, 1.65},
      {"ls", 21, 1.88},
      {"ww", 33, 1.9}
    };


    FILE *fp = fopen("person.stu", "wb+");
    fwrite(&ps, sizeof(ps), 1, fp);

    rewind(fp);
    Person p;
    while(fread(&p, sizeof(p), 1, fp) > 0){
        printf("name = %s\n", p.name);
        printf("age = %i\n", p.age);
        printf("height = %lf\n", p.height);
    }
    return 0;
}
  • 讀寫結構體連結串列
#include <stdio.h>
#include <stdlib.h>

typedef struct person{
    char *name;
    int age;
    double height;
    struct person* next;
} Person;
Person *createEmpty();
void  insertNode(Person *head, char *name, int age, double height);
void printfList(Person *head);
int saveList(Person *head, char *name);
Person *loadList(char *name);

int main()
{

//    Person *head = createEmpty();
//    insertNode(head, "zs", 18, 1.9);
//    insertNode(head, "ls", 22, 1.65);
//    insertNode(head, "ws", 31, 1.78);
//    printfList(head);
//    saveList(head, "person.list");
    Person *head = loadList("person.list");
    printfList(head);
    return 0;
}

/**
 * @brief loadList 從檔案載入連結串列
 * @param name 檔名稱
 * @return  載入好的連結串列頭指標
 */
Person *loadList(char *name){
    // 1.開啟檔案
    FILE *fp = fopen(name, "rb+");
    if(fp == NULL){
        return NULL;
    }
    // 2.建立一個空連結串列
    Person *head = createEmpty();
    // 3.建立一個節點
    Person *node = (Person *)malloc(sizeof(Person));
    while(fread(node, sizeof(Person), 1, fp) > 0){
        // 3.進行插入
        // 3.1讓新節點的下一個節點 等於 頭節點的下一個節點
        node->next = head->next;
        // 3.2讓頭結點的下一個節點 等於 新節點
        head->next = node;

        // 給下一個節點申請空間
        node = (Person *)malloc(sizeof(Person));
    }
    // 釋放多餘的節點空間
    free(node);
    fclose(fp);
    return head;
}

/**
 * @brief saveList 儲存連結串列到檔案
 * @param head 連結串列頭指標
 * @param name 儲存的檔名稱
 * @return  是否儲存成功 -1失敗 0成功
 */
int saveList(Person *head, char *name){
    // 1.開啟檔案
    FILE *fp = fopen(name, "wb+");
    if(fp == NULL){
        return -1;
    }
    // 2.取出頭節點的下一個節點
    Person *cur = head->next;
    // 3.將所有有效節點儲存到檔案中
    while(cur != NULL){
        fwrite(cur, sizeof(Person), 1, fp);
        cur = cur->next;
    }
    fclose(fp);
    return 0;
}
/**
 * @brief printfList 遍歷連結串列
 * @param head 連結串列的頭指標
 */
void printfList(Person *head){
    // 1.取出頭節點的下一個節點
    Person *cur = head->next;
    // 2.判斷是否為NULL, 如果不為NULL就開始遍歷
    while(cur != NULL){
        // 2.1取出當前節點的資料, 列印
        printf("name = %s\n", cur->name);
        printf("age = %i\n", cur->age);
        printf("height = %lf\n", cur->height);
        printf("next = %x\n", cur->next);
        printf("-----------\n");
        // 2.2讓當前節點往後移動
        cur = cur->next;
    }
}

/**
 * @brief insertNode 插入新的節點
 * @param head 連結串列的頭指標
 * @param p 需要插入的結構體
 */
void  insertNode(Person *head, char *name, int age, double height){
    // 1.建立一個新的節點
    Person *node = (Person *)malloc(sizeof(Person));
    // 2.將資料儲存到新節點中
    node->name = name;
    node->age = age;
    node->height = height;

    // 3.進行插入
    // 3.1讓新節點的下一個節點 等於 頭節點的下一個節點
    node->next = head->next;
    // 3.2讓頭結點的下一個節點 等於 新節點
    head->next = node;
}
/**
 * @brief createEmpty 建立一個空連結串列
 * @return 連結串列頭指標, 建立失敗返回NULL
 */
Person *createEmpty(){
    // 1.定義頭指標
    Person *head = NULL;
    // 2.建立一個空節點, 並且賦值給頭指標
    head = (Person *)malloc(sizeof(Person));
    if(head == NULL){
        return head;
    }
    head->next = NULL;
    // 3.返回頭指標
    return head;
}

其它檔案操作函數

  • ftell 函數
函數宣告long ftell ( FILE * stream );
所在檔案stdio.h
函數功能得到流式檔案的當前讀寫位置,其返回值是當前讀寫位置偏離檔案頭部的位元組數.
引數及返回解析
引數FILE * 流檔案控制程式碼
返回值int 成功,返回當前讀寫位置偏離檔案頭部的位元組數。失敗, 返回-1
#include <stdio.h>

int main()
{
    char *str = "123456789";
    FILE *fp = fopen("test.txt", "w+");
    long cp = ftell(fp);
    printf("cp = %li\n", cp); // 0
    // 寫入一個位元組
    fputc(str[0], fp);
    cp = ftell(fp);
    printf("cp = %li\n", cp); // 1
    fclose(fp);
    return 0;
}
  • rewind 函數
函數宣告void rewind ( FILE * stream );
所在檔案stdio.h
函數功能 將檔案指標重新指向一個流的開頭。
引數及返回解析
引數FILE * 流檔案控制程式碼
返回值void 無返回值
#include <stdio.h>

int main()
{
    char *str = "123456789";
    FILE *fp = fopen("test.txt", "w+");
    long cp = ftell(fp);
    printf("cp = %li\n", cp); // 0
    // 寫入一個位元組
    fputc(str[0], fp);
    cp = ftell(fp);
    printf("cp = %li\n", cp); // 1
    // 新指向一個流的開頭
    rewind(fp);
    cp = ftell(fp);
    printf("cp = %li\n", cp); // 0
    fclose(fp);
    return 0;
}
  • fseek 函數
函數宣告int fseek ( FILE * stream, long offset, int where);
所在檔案stdio.h
函數功能偏移檔案指標。
引數及返回解析
參 數FILE * stream 檔案控制程式碼
long offset 偏移量
int where 偏移起始位置
返回值int 成功返回 0 ,失敗返回-1
  • 常用宏
#define SEEK_CUR 1 當前文字
#define SEEK_END 2 檔案結尾
#define SEEK_SET 0 檔案開頭
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt", "w+");
    fputs("123456789", fp);
    // 將檔案指標移動到檔案結尾, 並且偏移0個單位
    fseek(fp, 0, SEEK_END);
    int len = ftell(fp); // 計算檔案長度
    printf("len = %i\n", len);
    fclose(fp);
    return 0;
}
#include <stdio.h>

int main()
{
    FILE *fp;
   fp = fopen("file.txt","w+");
   fputs("123456789", fp);

   fseek( fp, 7, SEEK_SET );
   fputs("lnj", fp);
   fclose(fp);
    return 0;
}

如果覺得文章對你有幫助,點贊、收藏、關注、評論,一鍵四連支援,你的支援就是江哥持續更新的動力