課程內容#
if 語句、switch 語句#
- (C 語言關係運算符)
- 0 → false;1 → true
- false 👉 0、NULL(null)、'\0'
- <= → =<
- ! 取非 (關係運算符)
- 要注意區分:位取反運算符~(位運算符)
- ⭐!! 非非操作:邏輯上的歸一化
- 將所有真值統一到 1,假值還是 0
- 順序結構
- 略
- 分支結構
IF 語句#
-
-
(表達式)
- 任何表達式都有邏輯返回值
- 對於賦值表達式 a = 123;
- 返回值為 a 變量的值 123
-
{代碼段;}
- 了解什麼是一條語句?
- 單語句:“;” 結尾的語句
- int a = 1, b = 234; // 是一條語句,“,” 前後分為為一個表達式
- 空語句:只有一個 “;”,沒有作用
- 複合語句
- 用花括號
- 即代碼段👆
- 單語句:“;” 結尾的語句
- 了解什麼是一條語句?
-
if else 語句
- 可以用問號表達式(三目運算符)來代替
- ...? ...: ...;
- 可以用問號表達式(三目運算符)來代替
SWITCH 語句#
-
-
變量 a 要有一個明確的唯一的整型值作為映射!
- 字母也有這樣的映射:ASCII 碼
-
進入 case 後,依次執行後續代碼
- 直到遇到break、continue 或結構末尾~
- 而不需要再判斷其後的 case 是否滿足
-
default 分支
- 當 case都不滿足時,會進入,相當於 if else 語句的 else
CPU 的分支預測#
從力扣題 -回文整數引出
船長超高時間效率代碼如下:
-
-
紅框處實現的就是 x<0
- 用來篩選小於 0 的整數,負數肯定不是回文整數
-
這段語句在操作系統內核裡出現:
-
-
!!(x) 邏輯歸一化,結果只會是 1 或 0
- 經常成立與否的情況會告訴正在處理程序的 CPU
⭐CPU 執行程序過程解析
- C 語言程序執行過程:編譯後生成可執行文件→加載到內存裡→CPU 執行
- cash 緩存
- 一級緩存:速度最快,而容量最小
- CPU 指令執行方式對比
- 早期串行(取址、指令預解析、寫數據、執行、寫回內存)
- 當今並行
- 示意圖如下左右:
-
-
對於 5 條指令
- 串行方式需要 5 * 5 = 25 個時鐘周期
- 並行方式只需要 5 + 4 = 9 個時鐘周期
-
當指令非常多時,提高效率將近 5 倍
-
- 所以!CPU喜歡順序結構
- 討厭分支結構(要少用 if else 語句)
- 分支預測問題
- 對於並行的方式,CPU 不知道分支結構下一步是誰,该提前加載哪一步?
- CPU 實際是隨機加載下一步
- 所以,如果預測錯了,後面就白做了,此時又要返回重新處理
- CPU 實際是隨機加載下一步
- 而__builtin_expect () 做的就是告訴CPU 哪種分支情況更易發生!!!
循環結構#
- WHILE 語句
- while () {代碼塊;}
- 先判斷,再執行
- do {代碼塊;} while (表達式); ←分號要注意!
- 至少執行一次
- while () {代碼塊;}
- FOR 語句
- for (初始化;循環條件;執行後操作) {代碼塊;}
- 讓循環變得很秀,由三部分組成
- 這三部分可以都不寫
- 例如:for (; ;);,那就相當於 while (1);
- 上面是一個死循環,啥都沒有
隨堂練習#
-
-
記得條件判斷冗餘檢查
-
輸入非法時(如 "q")陷入死循環問題
- 解決方法及思考過程詳見下文:思考點 1
-
代碼
-
冗餘版
-
-
❗這裡 scanf 的 \n 是隱患!要去掉 \n,否則需要多讀一個數
-
因為在輸入下個數時才讀到 \n
-
⭐參考博客:用 "% d\n" 要多讀一個數? —— scanf () 函數的那些坑
-
-
-
精簡版
-
-
-
①
-
②
- 熟悉 switch 語句結束的特性
- 代碼
-
-
-
代碼
-
對於 while 方式,如果首次判斷成立,則與 do...while 方式沒有區別
-
-
代碼
-
亮點筆記#
- ⭐對條件判斷語句進行冗餘檢查(剪裁),通常可以有更精簡的代碼
- 所有判斷表達式是否為 0 的情況,可以直接使用 if (! 表達式)
- 對於 if (表達式) 單語句,可以利用邏輯與
- (表達式) && 單語句
- 比如 i && printf (" "); ,用來在除第一次循環中輸出空格
- 單語句不能是 {代碼塊;}
- 對於 if (表達式) 單語句,可以利用邏輯與
- if...else 用三目運算符代替
- 所有判斷表達式是否為 0 的情況,可以直接使用 if (! 表達式)
- 一般將循環變量 i 定義在初始化部分:int i = 0
- 不需要提前放在前面
- 更規範、簡明:用變量之前再定義,不要離的太遠
- 作用域問題
- 只在循環裡用到該變量
- 不需要提前放在前面
- ++i 比 i++ 快?
- 能用 ++i 就用
- 從函數棧的角度:👇
- ++i 直接入棧 i+1 的值
- 而 i++ 的話,先入棧 i 再入棧 i+1
- 能用 ++i 就用
代碼演示#
分支結構(6.struct_program.cpp)#
-
-
a - b 可以代替 a == b 判等
-
if else 可以用三目運算符代替
-
-
輸出 1:false
- a++ 和 && 的運算優先級?
- ++ 優先級大於 &&
- a++ 外面的 () 為什麼沒有把 ++ 的運算優先級提高到比 && 先加呢?
- 是優先的
- 這裡輸出 false 與此無關~
- 應該對於 if 判斷來說,先判斷 a,再去 ++
- a++ 表達式的值是沒有 ++ 的 a
- a++ 和 && 的運算優先級?
-
輸出 2:a = 1, b = 0
- ⭐邏輯與 -- 短路規則
- 只要前面有假值,就不會往後走了【聰明人的做法】
- ⭐邏輯與 -- 短路規則
-
-
(接上 a = 1, b = 0)
-
輸出 1:true
-
輸出 2:a = 2, b = 0
- ⭐邏輯或 -- 長路規則
- 只要前面有一個真值,就不用了往後走了
- 要想執行後面的表達式,必須前面的表達式均為假值
- ⭐邏輯或 -- 長路規則
-
-
以空格為間隔(末尾無空格)的輸出方式參考
- 方式一:如果不是第一次循環,輸出前空格
- 首次更好得到:i == 0
- 可優化第 43 行:i && printf(" ");
- 方式二:如果不是最後一次循環,輸出後空格
- 方式一:如果不是第一次循環,輸出前空格
-
rand()
-
需引入 <stdlib.h>
-
輸出的是隨機的無符號數
- rand () % 100 可以隨機輸出 0-99 的數
-
⭐其實是固定的隨機
-
-
通過 srand() 設定隨機種子
-
srand(time(0))
- time(0)
- 獲取當前的時間,需引入 <time.h>
- 精確到 s,1970.1.1 至今的秒數
- time(0)
-
可以看出值是變化的了,但計算機中沒有真正意義上的隨機
-
-
-
-
-
優化版
-
上述代碼有三處可以簡化!兩個 if 都可以去掉
-
-
⭐~
- 位運算與取餘的轉換:% 2 等價於 & 1
- % (2 ^ n) 等價於 & (2 ^ n - 1)
- +1 與真值 1 的等價
- 邏輯與的短路規則
- 位運算與取餘的轉換:% 2 等價於 & 1
-
循環結構(7.cpp)#
判斷回文整數(十進制)
-
-
記得對負數做特判!
-
如果想判斷二進制下的回文整數呢?
-
將圓圈處都改成 2 即可
- 計算機底層其實都是以二進制存儲數據的
- 不管什麼進制下,都可以判斷回文
-
base 進制下回文整數判斷
-
-
計算整數位數(十進制)
- while 循環和 do...while 循環的細微區別比較
- 關鍵在於第一次循環的條件是否成立~
-
- 當輸入非 0 數字時,digit 和 digit2 沒有區別
-
- 當輸入數字 0 時,有區別
-
-
所以有 0 的情況判斷位數
* 用 do...while
* 或者特判
-
- 關鍵在於第一次循環的條件是否成立~
附加知識點#
-
switch 語句
- case 後面不允許聲明變量
- default 的末尾也要加 break 嗎
- 可有可無,加 break 是為了統一風格
- Python 不支持 switch 語句
-
C 語言中,如果 main 函數的末尾沒有 return 語句將會有什麼影響?
- C99 開始,基本沒影響,標準要求等價於自動補上return 0;
- 而 C99 之前,如果控制抵達 main 函數結尾而沒有 return,則為行為未定義。
-
浮點數判等
- 不直接用 ==,會不準確,利用差值是否小於某個極小值來判等
-
__builtin_expect () 還有 6 + 個兄弟
-
-
位權決定左移右移的倍數
- 十進制,左移一位,*10
- 二進制,左移一位,*2
思考點#
-
💡(解決)對於 5.if.cpp,即隨堂練習 1。輸入非法值,如字母‘q’,會陷入無限循環
-
與‘q’的 ASCII 碼有關嗎?
- 無關~
- 以 % d 形式輸出‘q’,看起來是一個地址,單次運行是固定的,應該是初始化 n 時隨機分配的一個值
- 如果輸入正常值再輸入錯誤值,此時該值就是之前的 n 值
- 因為其實沒有讀入值來改變 n
-
讀不進‘q’值
- scanf 的返回值為 0
- 但是也不會移向下個輸入,類似第一講裡的 %[^\n] 遇到 \n 無限循環事件
-
⭐當然! 同樣可以使用 getchar () 吞掉這個非法值
- 輸入 qw 屬於非法嗎
- 是的,需要 getchar () 吞兩次
- 輸入 qw 屬於非法嗎
-
改後代碼
-
-
getchar () 放在 if (!ret) 裡更好,只有在讀入非法字符時才吞字符
-
上圖的代碼:getchar會吞空白符和非法字符
-
下圖的代碼:只會吞非法字符
-
- 為了演示吞掉的字符沒有空白符只有非法字符,這裡加了 printf 函數
-
-
-
-
-
-
💡同樣,對於 5.if.cpp(有循環,需手動停止),用 > output 將標準輸出重定向時,會遇到輸出不寫入重定向的文件的問題
- 具體情況已在海賊 QA 提問,期待探討:輸出重定向在什麼條件下才將輸出寫入文件呢?(stdout、./a.out > output、Ctrl+C/Ctrl+D)
- Ctrl+C 不寫入,Ctrl+D 寫入
Ctrl+C:Linux 下默認的中斷鍵,當鍵入此鍵時,系統會發送一個中斷信號給正在運行的程序和 shell。
Ctrl+D:Linux 下標準輸入輸出的EOF。在使用標準輸入輸出的設備中,遇到該符號,程序會認為讀到了文件的末尾,因此結束輸入或輸出。
- 如果有非法輸入導致有大量輸出時,Ctrl+C 也可以寫入
- 疑問:這是因為緩存不夠,提前強制輸出到 output 文件了嗎?
- 是這樣理解
- 疑問:那麼這部分寫入 output 文件的輸出僅僅是緩存溢出的輸出,還是 Ctrl+C 前的所有輸出呢?
- 溢出的
- 疑問:這是因為緩存不夠,提前強制輸出到 output 文件了嗎?
- 代碼 5.switch.cpp:為啥輸入字母之類的會無限循環?
- 同 5.if.cpp,見思考點 1
Tips#
- OJ 做題方式
- 在 vim 裡編寫代碼
- cat 顯示代碼複製出來
- 提交
- 經典題:已知年月日判斷是否合理
- 有沒有必要用那麼多的 if else?
- 可以用空間換時間:把每月的天數用數組存起來
- 參考工具書第 6 章 6.4 節