主要是我自己編寫邊學慢慢累積的,說真的也找不太到相關的資源,要搜尋也不知道下什麼關鍵字,問別人其實也很難在一時半刻裡解釋清楚,所以就拿來發一篇吧 OuO

前言

期中加資格考的爆炸禮拜終於結束,其實比我想得還要容易,雖然我都很沒把握,因為我幾乎不狂寫考古題,而是以觀念取勝,有沒有取到就不確定了。

寫程式很簡單,拿來解決問題就有些難度,最難的是用優雅易懂且嚴謹的程式碼來解決問題,寫得好絕對遠遠好過寫得快。

關於這個主題,其實很早就打算寫了,只是一直擱置連架構都沒出來,直到最近有幫同學看一下程式碼,所以覺得這個觀念還是頗為重要,所以整理一下發個文,主要也希望可以幫助比較沒經驗的人,但也不限於此,我相信也會有畢業於資訊工程相關科系而沒有這些觀念。

當然在整個程式界我也不是老手,幾乎沒在開源貢獻,更沒參與過業界,所以很多的概念是我在大學時期慢慢累積起來的,主要來源就是一些開源的專案還有自身的經驗,不能說涵蓋得很全面,也不是說我這套最棒了大家看了之後一定要照這些規則,這篇從來就不是規則,而是我在寫了幾年程式後整理出來讓自己開發更為流暢的一些想法,希望大家能夠來互相討論給與意見。

架構會以不同的主題當作探討,雖然全部都是在講 programming sense,內容因為相當廣泛,切成不同文章又會讓某些部分零碎化,所以請善用標題跟右方的目錄來幫助閱讀。順序以 “工具”,“coding style”,“習慣養成”,“抽像化”,“寫程式的過程”,“除錯”。並主要以 C 語言當作講解範例。

雖然雜但是請記得一個中心思想:

用程式碼來溝通 (Communication Through Code)

工具

「工欲善其事,必先利其器」《論語·衛靈公》 善用工具真的頗為重要,使用得當可以大量減少重複性的動作,如果是寫一般的小程式的話我會建議可以玩看看 VIM 設定可以參考 我的設定或直接問我,可以大量減少手部的負擔。大型一點的我會推薦 VSCode,因為內容會偏多所以我就用一個副標題來說明了。

vi…

vi…

OuO
1

VSCode

VSCode 其實不太像一般的 IDE,它更像一個單純的文字編輯器,只是有相當多好用的外掛功能,基本的包含了對各種語言的程式碼的關鍵字 highlight、檔案架構樹、搜尋取代功能、編碼轉換、coding style 自動重新排版。以下附上一些我目前服役中的外掛功能,使用方法就請各位自己前往查看了:

  • alefragnani.bookmarks 🔗: 可以在想要標記的程式碼上加入書籤,這個在瀏覽大型專案需要跳來跳去 trace 程式碼下可以幫很大的忙,至少不用再記是第幾行了。
  • coenraads.bracket-pair-colorizer 🔗:連結對應的括號,這在括號一堆的語言 (C, C++, Java, Lisp) 相當有幫助,可以一眼就看出在哪個有效範圍 (scope)。
  • streetsidesoftware.code-spell-checker 🔗:幫忙檢查英文拼字。
  • tabnine.tabnine-vscode 🔗:程式碼補全建議。
  • gruntfuggly.todo-tree 🔗:收集註解中有 TODO 標籤的地方。
  • donjayamanne.githistory 🔗:Git 系列。
  • eamodio.gitlens 🔗:Git 系列。
  • zhuangtongfa.material-theme 🔗:耐看主題。
  • vscode-icons-team.vscode-icons 🔗:美美的檔案圖示。

名稱拿去搜尋即可

名稱拿去搜尋即可

OuO

Shell

很多指令都是可以幫助懶惰的人,目前我還沒有遇過我想要但是沒有的指令。

重新導向 (Redirection)

寫程式時會常使用重新導向 (Redirection) 的方式來減少標準輸入 (stdin) 的次數,用法也相當直觀,只需要先將測試輸入先打在一份文檔 (例如:input.txt) 中,再使用以下指令執行程式即可。

$ ./queue < input.txt

若要把標準輸出 (stdout) 存到另一份文件時呢,當然也是使用重新導向。這適合在輸出很多時或是需要搜尋結果時使用。

$ ./queue < input.txt > output.txt

Coding Style

這是一個看似微小卻非常重要的細節,尤其是需要別人幫忙 review 甚至是合作時都需要事先講好共用的 coding style,一來格式統一閱讀時不需要轉換,二來可以避免因為工具自動調整造成不必要的程式改動紀錄。

看別人的程式碼時最怕遇到沒有縮排的…

這裡涵蓋的範圍包括了縮排、空格、括號、命名。

pythonize … 別亂學 XDD

pythonize … 別亂學 XDD

OuO
2

Google Coding Style

若要偷懶,強烈建議就直接用最多人使用的就好,很多工具都有辦法幫忙重新排版程式碼,這裡展示如何用 VSCode 來設定 Google 使用的 coding style,規定的格式細節可參考 Google C++ Style Guide

開啟設定

開啟設定

OuO

搜尋 format style,將欄位改成 Google

搜尋 format style,將欄位改成 Google

OuO

使用格式化可以用右鍵或是直接按對應的快捷鍵

使用格式化可以用右鍵或是直接按對應的快捷鍵

OuO

習慣養成

傻瓜都可以寫出機器能讀懂的代碼,但只有專業程序員才能寫出人能讀懂的代碼。
— 李先靜 《系統程序員成長計劃》3

Coding Style Again

用工具是很方便沒錯,不過盡量還是養成習慣而不依賴工具,這裡介紹幾個比較常見的格式:

空行 (Blank)

把所有操作寫在同一個函式裡面通常不容易辦到,因此不免會有某些區塊在做相關的事,合理使用空行能夠讓讀者很容易就看出程式碼的這些區塊的用途。

縮排 (Indent)

我以前偏好 4 個空格,但是最近有往兩個空格移動的趨勢,另外 Google 也是以 2 個空格為主。然而 Linux kernel 是使用 tab,這裡其實只要跟合作的夥伴們講好統一使用一種即可,就不提有統計指出用 space 賺的錢比用 tab 的人多了 4

If Statement

加上空格們

if(condition) {   // Bad
if (condition){   // Bad
if(condition){    // Doubly bad
if (condition) {  // Good

加上大括號們

這裡我會比較嚴格規定自己,就算是只有一行也要加,這樣比較方便擴充,例如臨時要加上 printf 時就不用再加。

if (condition) {
  foo;
} else {
  bar;
}

Loop Statement

加上空格們

注意 ; 前不要後要。
這裡用 i++++i 其實基本上沒有效能差異 5,編譯器會幫你最佳化,我更喜歡 i++ 因為要改成 i+=2 之類的比較方便,另外也是有 i-=-1 6 這種邪教…

for (int i = 0; i < some_number; i++) {
  printf("OuO\n");
}

Pointer 變數宣告

int*x;int *x;int* xint * x 都是可編譯的寫法,在 C 中 int *x; 更為常用。C++ 中反而是 int* x; 7

命名 (Naming Convention)

檔案名稱、變數 (Variable)、函式 (Function)

使用 snake_case 並取有上下文關係的名字,例如 flagcount 就沒有上下文,會不知道這個變數要用來存放什麼東西。
e.g., http_server_logs.htable_name

Google 的函式也可用 UpperCamelCase,但我個人比較習慣 snake_case,另外函式須以動詞開頭以表明動作。
e.g., add_table_entry

Struct, Class

使用 UpperCamelCase
e.g., UrlTableProperties, TableInfo

常數 (Constant)

使用全大寫並以 _ 連接。
e.g., MAX_ROW_SIZE

初始化 (Initialize)

寫 C 語言時要注意變數的初始化,以免結果跟自己所想的不一樣,這是因為在規格書中的定義中提到,只有全域變數或是靜態 (static) 變數會被初始化,其他未初始化的則不會有明確的初始值。 C99 §6.7.8.10 8。在陣列的初始化若要全部定義為 \0,我們可以直接在宣告中使用 {} 即可,因為規格書中有規定若初始化的個數不足則會比照靜態變數 C99 §6.7.8.21 8:

// 每個變數宣告時搭配初始化
int n = 0;
// 陣列初始化以下都可以
char que[26] = {'\0'};
char que[26] = {0};
char que[26] = {};

註解 (Comment)

請盡量使用英文來註解,因為英文比較不像中文那樣一詞多義,可以較明確的敘述,若執意要用中文就需要注意編碼,目前主流應該是使用 UTF8。內容部份可以多記錄一點上下文而不是僅僅該行程式碼做了什麼事,通常註解會拿來說明整個函式,會需要一行一行註解的情況比較少並會用高可讀性的程式馬來替代。我之前有找到一篇非常詳盡的指南 9 可以參考看看 (雖然我也沒有仔細讀完就是了@@

寫法上注意空格即可。

// OuO
/* OuO */

副作用 (Side Effect)

副作用 ( Side Effect) 聽起來好像很不妙,簡單來說就是會在函式內部修改到參數的情況。其實這在一般程式語言中頗為常見,沒有這項功能的話程式會變得相當難寫,例如 Functional language 寫起來就頗耗費腦力。

在函式宣告時點出副作用

但是在開發過程需要適時隔離變化,C 語言提供了 const 修飾字來標示該參數在函式中不會被修改。以下範例中的 rearorig 不會被修改到內容,所以可以利用 const 來提醒函式呼叫者該參數不會被改動,反之 front 就有機會被改動。詳細的排列組合可以參考 10

void deque(char *orig, int *front, int *rear) {
  if (*front == *rear) {
    printf("Empty");
  } else {
    printf("%c\n", orig[*front]);
    *front = *front + 1;
  }
}
// rear, orig 皆是指向一個唯讀參數 (read-only parameter)
// 的唯讀指標 (read-only location)
// 若嘗試在函式中修改 `rear` 或 `*rear` 就會得到編譯錯誤
void deque(
    const char * const orig,
    int *front,
    const int * const rear) {

需要修改的才傳入其指標 (Pointer)

這裡要先宣導一個觀念就是 C 語言只有 Pass By Value,也就是只會複製傳入變數的數值,傳入指標也是一樣,我們只是傳入了指標這個數值,而透過這個指標的取值 (dereference) 可以改動對應的記憶體位置 C99 §6.5.2.2.4 8。而因為 C 的函式回傳值只限一個,所以大部分會使用修改參數的方式來回傳數值,而真正的回傳值就用來表示函式是否正確執行,否則就回傳對應的錯誤碼。

上面那樣改起來好像太奇葩,更好的解決方式是要先思考,把將要在函式內部更動變數在傳入其指標,其餘的只須把數值傳進去即可。當然若是傳入陣列就還是需要指標就像 orig

// 因為最後一個參數不會變動而且只需要其數值
// 所以直接以常數傳入,當然函式內部就不需要取值了
void deque(
    char const * const orig,
    int *front,
    const int rear_value) {
  if (*front == rear_value) {
    printf("Empty");
  } else {
    printf("%c\n", orig[*front]);
    *front = *front + 1;
  }
}

多檔案

避免重複 include

專案擴張下,必然會用到多檔案,這時候就會拆成很多的 .h, .c 檔,.h 用來作為界面的溝通,這時候一定要使用 macro ( Include guard) 防止重複 include。以下為檔名為 file.h 的內容範例:FILE_H, _FILE_H, __FILE_H__ 都有人用,但一般以第一種為主即可。

#ifndef FILE_H
#define FILE_H

/* ... Declarations etc here ... */

#endif /* FILE_H */

static 修飾內部函數

如果在多檔案下函數並不是要開放給其他檔案呼叫時,請加上 static 來保證只有當前檔案內部可以看到那個函數。

static void my_local_function() {
  printf("OuO\n");
}

避免使用危險的函式們

C 語言給予使用者相當大的彈性但用起來需要知道自己在做什麼,否則會有許多安全性漏洞,有一些函式很容易造成誤用,連資深工程師也常用錯,因此蠻多專案直接把不安全的函式禁用,例如 Git 11,Intel 的 safestringlib 也有一個完整列表 12。以下給出一些較常見的。

  • gets() 13 C11 已經不支援,請改用 fgets()
  • strcpy() 沒有長度偵測。
  • strncpy() 有長度但是不會在結尾補上 \0 14
  • ato*() 改用 strto*() 利於錯誤處理 C99 §7.20.1 8

其他習慣

  • 盡量不使用全域變數

{{ 下一篇傳送門:Programming Sense (2) }}

  • ⊛ Back to top
  • ⊛ Go to bottom