# OOP物件導向 程式碼撰寫指南 ## 作業繳交 我們的作業評分採用 [Online Judge](https://hackmd.io/Qi_R6aWVQz-LH7GJLq85QQ?view) 的評分結果,但是還是**強烈建議**學生上傳到 Moodle 上 >[!Warning] >若學生到時候沒有上傳檔案到 Moodle,則**視為放棄爭取重新批閱、調整分數的權利** 歷年的成績計算方式(正課): * 段考的成績大約佔比是 70~80%,合計6題,分數只有0或20分 (全對或全錯) 分數通常是0或是20的倍數,特殊題目可能會部份給分(但幾乎不會) * 作業也是一樣,每題都只有0或100分,在根據總題目數量分布 Ex. 整學期有60題,你答對58題:`20*(58/60) = 19.34`(假定作業佔20%總分) 所以班上會有很多人的成績是相同的,例如作業成績都是滿分,考試都對 3 題之類的 * 期中考的及格率約為 30% (50人中15人 60 分以上),期末考可能會高一點 * 期末結算大概會有 50% 的人成績是未滿60的,但至多當最後面30%的人 (總分低於60++不等於++被當,不用擔心全班都及格還要當30%的人,不會發生該情況) >[!Tip] 上傳到 Moodle 的意義是當成績出現爭議時,助教會嘗試手動檢閱學生提交的原始碼 原始碼必須是在作業截止日期前上傳到 Moodle 的才有效 :::danger 因為助教群要從 50% 的人挑出一部分的人可以PASS: **若且唯若**,當分數接近時,助教群會把不及格同學們的原始碼從**Moodle**下載下來,根據是否使用clang-format排版、Namespace的使用細節、Header檔案的宣告、註解撰寫...等,上課提及的細節來額外算分。這個分數影響非常小,通常是在總成績+-幾分,但會影響你在不及格的人群中是不是比較守規矩的學生。 Ex. 如果有兩個同學的成績相同,且總成績都未滿60,及格名額剩下一個的情況下: * A有上傳原始碼到 Moodle 上,B沒有,則A及格。(**上文提及,未上傳=視同放棄爭取調分**) * 都有上傳就看 format、namespace、header declare、註解撰寫的細節 * 分不出來助教才會寫信建議老師讓多位同學都及格 ::: ### Step 1: 確保原始碼檔案已格式化 在打包解決方案前,請使用自動格式化工具格式化,格式化後的程式碼具有統一的格式,不管是在審閱還是多人協作上都非常重要。 確保Visual Studio的ClangFormat支援打開並自動執行,*每個開發環境只須設定一次*。 位於`工具` `➜` `選項`下,`文字編輯器` `➜` `C/C++` `➜` `程式碼樣式` `➜` `格式` ![image](https://hackmd.io/_uploads/rk9rOsm26.png) 針對每次作業的解決方案, 請複製助教提供的[格式化設定檔](https://gist.github.com/silent4v/25b819173f539d4037a0d4023a6613e3)內容,放置於**解決方案**資料夾中,與 `XXX.sln`同路徑,並改名為`.clang-format` :::warning Windows 用戶要注意,預設會隱藏副檔名,所以建立了`.clang-format`檔案,實際上名稱會是`.clang-format.txt` 參考微軟的[如何顯示副檔名](https://support.microsoft.com/zh-tw/windows/windows-%E4%B8%AD%E7%9A%84%E4%B8%80%E8%88%AC%E6%AA%94%E6%A1%88%E5%90%8D%E7%A8%B1%E5%89%AF%E6%AA%94%E5%90%8D-da4a4430-8e76-89c5-59f7-1cdbbc75cb01),把「顯示副檔名」跟「隱藏的檔案」都勾起來,並檢查檔案系統顯示的類型是不是 `CLANG-FORMAT` ::: ![image](https://hackmd.io/_uploads/B1hSPoX2a.png) 對每個Source File與Header File進行格式化。 快速鍵`Ctrl+K` 按下後 `Ctrl+D`,即可自動格式化檔案,在開發過程中也請多加利用。 Before: ![image](https://hackmd.io/_uploads/r1B05jXhT.png) After: ![image](https://hackmd.io/_uploads/SJG-ji7hp.png) 如果啟用鍵盤配置`Visual Studio Code`,也可以使用快捷鍵`Shift+Alt+F`。 可參考 [MSDN](https://learn.microsoft.com/zh-tw/visualstudio/ide/reference/options-text-editor-c-cpp-formatting?view=vs-2022) ### Step 2: 確保可執行檔案已被清除 專案(Project)需要建置(Build)出可執行檔(Executable)才能執行與偵錯。雖然開發的目的是得到能夠發布並執行的檔案,但是: * 可執行檔體積通常較大。 * Debug設定下的執行檔會加入非常多的特殊指令以便偵錯,容易被誤判病毒,造成批改不便。 :::danger 不要上傳 `.obj` `.exe` `.dll` 等檔案,也不要包含 `.vs` `x86` `x64` 等資料夾 ::: 所以打包上傳之前,**請清除所有建置**。 `方案總管` `➜` `解決方案XXXOOO` `➜` `右鍵選單` `➜` `清除方案` ![image](https://hackmd.io/_uploads/HJSSAim3a.png) ### Step 3: 打包整個Solution(解決方案) 請將 * 所有Project資料夾( 每一題就是一個 Project ) * `.clang-format` * `XXX.sln` 打包成單一zip檔案,檔名為`學號_姓名.zip`。 e.g. `M11215006_蘇泓嘉.zip` 如下流程 `方案總管` `➜` `解決方案XXXOOO` `➜` `右鍵選單` `➜` `在檔案總管中開啟資料夾` ![image](https://hackmd.io/_uploads/HkaiRoXnT.png) 會看到Solution的資料夾,選取所有該上傳的檔案。 ![image](https://hackmd.io/_uploads/H1Oi1nmha.png) 右鍵 `➜` 傳送到 `➜` 壓縮的(zipped)資料夾 ![image](https://hackmd.io/_uploads/HJ0Cg3X2a.png) 依照命名規範命名zip檔案。 ![image](https://hackmd.io/_uploads/ryTlr372a.png) ## 程式碼格式指南 :::info 隔壁組採用的[評分標準](https://hackmd.io/@5wkgZFOvS96Y7XW3W0EADA/BkET0Lgnp),且平時就會計算排版的成績 本組助教群認為使用統一的工具進行處理才是合理的行為,也信任同學們平時就有開啟排版的功能,所以僅在決定及格名單時才會檢查並進行扣分 ::: 同學們可以參考 [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) 的開發指南 ![Google-Style](https://hackmd.io/_uploads/Skm87oQhT.png) 請使用 Clang-Format 進行排版,助教群提供一組 Clang-Format 設定檔,請放在專案的根目錄,檔名為`.clang-format`: ```yaml= # see https://clang.llvm.org/docs/ClangFormatStyleOptions.html --- BasedOnStyle: LLVM Language: Cpp Standard: c++14 UseTab: Never IndentWidth: 4 ColumnLimit: 160 # Sort Headerfile SortIncludes: true IncludeBlocks: Merge IncludeCategories: # Headers in <> without extension. - Regex: '<([A-Za-z0-9\Q/-_\E])+>' Priority: 4 # Headers in <> from specific external libraries. - Regex: '<boost\/' Priority: 3 # Headers in <> with extension. - Regex: '<([A-Za-z0-9.\Q/-_\E])+>' Priority: 2 # Headers in "" with extension. - Regex: '"([A-Za-z0-9.\Q/-_\E])+"' Priority: 1 # Namepsace, Template, Function, Class Declare AccessModifierOffset: -4 AllowShortFunctionsOnASingleLine: Empty AllowShortLambdasOnASingleLine: Empty AlwaysBreakTemplateDeclarations: Yes FixNamespaceComments: true # Alignment BreakBeforeBraces: Custom BraceWrapping: AfterCaseLabel: true AfterClass: false AfterControlStatement: MultiLine AfterFunction: false AfterNamespace: false BeforeCatch: false AlignAfterOpenBracket: AlwaysBreak AlignConsecutiveAssignments: None AlignConsecutiveBitFields: Consecutive AlignEscapedNewlines: Left BreakConstructorInitializers: BeforeComma IndentPPDirectives: AfterHash PointerAlignment: Left # If use Windows Visual Studio, DON'T Enable the following options # AlignArrayOfStructures: Right # PackConstructorInitializers: NextLineOnly ``` ### 摘要說明 助教群針對基礎的 Coding Style 進行一些調整,主要包含 * 縮排的長度 * 預設的單行長度限制 * 標頭檔的排序順序 * 部份修飾元的縮排處理 *** 除了以上的 Style 建議,我們也希望同學們額外遵守以下的限制: * 請避免在全域直接使用 namespace ```cpp!= /* Bad Example */ #include <iostream> using namespace std; int main() { cout << "Hello, World!" << endl; return 0; } ``` 請明確引用 namespace, 或是在需要使用到的 Block 中導入 ```cpp!= /* Solution 1 */ #include <iostream> int main() { std::cout << "Hello, World!" << std::endl; return 0; } /* Solution 2 */ #include <iostream> int main() { using namespace std; // import namespace here std::cout << "Hello, World!" << std::endl; return 0; } ``` * 前置處理器相關 :::info `#pragma once` 並不是所有編譯器都有通一的實現,因此建議使用Define Check來檢查 ::: 請不要使用 `#pragma once` 來保護標頭,請使用 `#ifndef ... #endif`。 ```cpp!= #ifndef EXAMPLE_HEADER_H #define EXAMPLE_HEADER_H // Your codes here... #endif ``` ### 程式碼排版 使用自動化工具來對程式碼進行統一的格式排版,產出格式統一且易閱讀的程式碼。 ## 程式碼註解指南 此處助教群引用 *Robert Cecil Martin(Uncle Bob)* 在其著作 *《Clean Code: A Handbook of Agile Software Craftsmanship》* 中的重要觀點 ### 1. 註解不能美化糟糕的代碼 優秀的代碼應該是可讀且易於理解的,而不是通過添加註解來補救其缺陷。如果代碼需要註解來解釋它在做什麼,那麼應該重新考慮如何改進代碼的清晰度。 * 反例:使用註解去說明複雜的邏輯 ```cpp= int d; // elapsed time in days // Calculate elapsed time in days between two time points // Note: time1 and time2 are in format YYYYMMDD if (((year2 - year1) * 12 + month2 - month1) * 30 + day2 - day1 > 30) { d = ((year2 - year1) * 12 + month2 - month1) * 30 + day2 - day1; } ``` * 正例:通過明確的變數與函式名稱,且將大功能拆解為多個邏輯簡單、意圖明確的小功能 ```cpp= int calculateElapsedDays(Date startDate, Date endDate) { return endDate.toJulianDay() - startDate.toJulianDay(); } ``` > 應該盡可能使用有意義的宣告名稱,讓程式碼可以明確表達意圖 ```cpp= // 計算立方體的體積,參數是邊長 double calcVolume(int length) { /* Do something */ } ``` ```cpp= double calcCubeVolume(int edgeLength) { /* Do something */ } ``` 以上兩例中,`calcVolume` 可能存在其他意思(計算什麼的體積?),所以需要額外的註解說明 而`calcCubeVolume` 則清楚的指出要計算的是立方體的體積 ### 2. 代碼應該盡量避免註解 好的代碼是不需要註解的。每當感到需要添加註解時,首先考慮是否可以通過重構代碼(例如,使用更具描述性的變量名稱、函數名稱或者更清晰的代碼結構)來使得註解變得多餘。 * 反例:在程式碼的意圖明確的情況下,進行過多解釋 ```cpp= int sum = 0; // Initialize sum to zero sum = a + b; // Add a and b and store the result in sum return sum; // Return the computed sum ``` * 正例:程式碼足以明確表達意圖,可以省略註解 ```cpp= int sum = a + b; return sum; ``` ### 3. 不良註解的例子 **冗余註解**:解釋了代碼已經明確說明的事情。 **誤導註解**:不精確或已過時的註解。 **義務註解**:只是因為過程規定而添加的無用註解。 **日誌式註解**:在代碼中保留大量的更新、修改歷史註解。 * 反例 1 - 日誌型註解 > 因為現代開發通常會導入CVS - 版本控制軟體,而如何寫好change log又是另外一門知識 ```cpp= /* * 2018-06-01: Added function (John Doe) * 2018-07-02: Fixed bug XYZ (Jane Smith) * 2018-08-03: Improved performance (John Doe) * 2019-01-04: Updated algorithm to include case ABC (Jane Smith) */ void complexFunction() { } ``` * 反例 2 - 誤導型註解 ```cpp= /** * @brief 計算兩個日期的差異天數 */ uint32_t dateDiff(Date &d1, Date &d2) { return abs(d1.unixTimestamp() - d2.unixTimestamp()); } Date d1('2/22/2024, 1:03:32 AM'); Date d2('2/23/2024, 8:04:57 AM'); dateDiff(d1, d2); // 錯誤,因為計算是使用 timestamp,實際上是回傳兩個時間相差的秒數 ``` * 反例3 - 冗餘註解 以一個簡單的函數為例,展示避免不必要註解的寫法: ```cpp= // Bad example: Redundant comments // Calculates the sum of two numbers int addTwoNumber(int a, int b) { return a + b; // Returns the sum of a and b } ``` ### 4. 註解存在的正當理由 :::warning 請注意,該指南不是跟你說「盡量不要寫註解」,而是 **「註解應該是在無法使用程式明確表達意圖,或是有程式碼以外的意圖需要表達時」** > 我們強調註解應當用於適當的情境,特別是當代碼本身無法清楚地表達開發者的意圖,或者當存在代碼本身無法涵蓋的背景信息時。 ::: 下方以 [Linux Kernel Source Code](https://github.com/torvalds/linux) 為範例: - **法律註解**:有時候需要在代碼中添加版權信息和版權聲明。 ![image](https://hackmd.io/_uploads/ByC9k3m36.png) - **提供信息**:解釋代碼無法告訴我們的信息,如某個複雜算法的背景。 ![image](https://hackmd.io/_uploads/HJvVe3736.png) - **警告**:提醒其他程序員某些行為可能有危險。 ![image](https://hackmd.io/_uploads/B1z1bn7hp.png) - **TODO 註解**:標記尚未完成或將來要改進的地方。 ![image](https://hackmd.io/_uploads/H1WHMnQn6.png) - **強調**:有時,對於那些不顯而易見的代碼行為,添加註解可以提醒程序員注意。 ![image](https://hackmd.io/_uploads/SkwLz37ha.png) ## 作業常見問題 助教群會依照同學們的問題回報,將可能遇到的情況與說明更新於此。 ### OJ常見問題 #### Wrong Answer可能遇到的問題 - 輸出不完整 :::info 請為每個題目考慮***測資會有多行多筆***。 顯示你的Program沒有輸出, 但答案是XXXOOO。 依照OJ的判斷邏輯,參考以下的流程。 ::: ```mermaid graph TB subgraph "範例測資" direction LR 1A["第一筆Excepted:123 Got:123"]--> 1B(["第二筆Excepted:456 Got:(無)"])--> 1C("然後就沒有然後了") end subgraph "隱藏測資1" direction LR e1A["第一筆Excepted:123456 Got:123456"]--> e1B(["第二筆Excepted:456789 Got:(無)"])--> e1C("然後就沒有然後了") end subgraph "隱藏測資2" direction LR e2A["第一筆Excepted:0011 Got:0011"]--> e2B(["第二筆Excepted:0022 Got:(無)"])--> e2C("然後就沒有然後了") end start(("Start"))--> 範例測資-->1result["顯示`Excepted:456 Got:(無)`"]--> 隱藏測資1-->e1result["顯示`Excepted:456789 Got:(無)`"]--> 隱藏測資2-->e2result["顯示`Excepted:0022 Got:(無)`"]--> flow_end(("End \n得到3個WA")) ``` ## 程式碼排版範例(使用Clang-Format) ```cpp= #include <iostream> // IndentPPDirectives #ifdef WIN32 # include <windows.h> #endif // SortIncludes #include "A/MyHeader.h" #include "B/AnotherHeader.h" #include <boost/asio/io_context.hpp> #include <boost/asio/ip/tcp.hpp> #include <boost/asio/signal_set.hpp> #include <boot #include <vector> // AlignConsecutiveMacros #define SHORT_NAME 42 #define LONGER_NAME 0x007f #define EVEN_LONGER_NAME (2) #define fooo(x) (x * x) #define baar(y, z) (y + z) // AlignEscapedNewlines #define PPP \ int aaaa; \ int b; \ int dddddddddd; namespace LevelOneNamespace { namespace LevelTwoNamespace { struct AAAAAAAAAAAAAAAAAAAA { // AlignConsecutiveDeclarations int a; int bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb; std::string cccccccccccccccccc; }; // AlwaysBreakTemplateDeclarations template <typename T> T foo() {} // clang-format off template <typename T> T foo(int aaaaaaaaaaaaaaaaaaaaa, int bbbbbbbbbbbbbbbbbbbbb) {} // clang-format on // AllowShortEnumsOnASingleLine enum : unsigned int { AA = 0, BB } myEnum; // SpaceBeforeInheritanceColon class B : public E { private: // AlignArrayOfStructures struct AAAAAAAAAAAAAAAAAAAA test[3] = { {56, 23, "hello"}, {-1, 93463, "world"}, { 7, 5, "!!"} }; // AlignTrailingComments, AlignConsecutiveDeclarations, // QualifierOrder, QualifierAlignment, // AlignTrailingComments static char const* variable; // very important variable void* const* x = nullptr; // not so important variable char const* anotherVariable; // another comment int a = 1; // another variable // used for this, this, and that int longComplicatedName = 4; int b = 3; protected: // AlwaysBreakAfterReturnType, QualifierAlignment constexpr static inline int function(int a, int b) { return (a + b) / 2; } // AllowShortFunctionsOnASingleLine static bool shortFilter(AAAAAAAAAAAAAAAAAAAA v) { return v.a != 4; } void empty() {} // IndentWrappedFunctionNames std::map<std::basic_string<wchar_t>, std::vector<std::pair<char, int>>> func(AAAAAAAAAAAAAAAAAAAA* v); public: // SpaceBeforeCtorInitializerColon explicit B() : a(9) {}; // PackConstructorInitializers explicit B(int _a, int _b, int _c, std::vector<std::string> str) : a(_a), b(_b), longComplicatedName(_c), anotherVariable(str[0].c_str()) { // AllowShortIfStatementsOnASingleLine, SpaceBeforeParens if (_c) anotherVariable = nullptr; if (_a) anotherVariable = "baz"; else anotherVariable = "bar"; } // AllowAllParametersOfDeclarationOnNextLine // BinPackParameters int myFunction(int aaaaaaaaaaaaa, int bbbbbbbbbbbbbbbbbbbbbbb, int ccccccccccccc, int d, int e) { int myvar = aaaaaaaaaaaaa / 10; long anothervaw = d % 2; // comment char* msg = "Hello all"; // AlignOperands myvar = bbbbbbbbbbbbbbbbbbbbbbb + ccccccccccccc + aaaaaaaaaaaaa; // AllowShortCaseLabelsOnASingleLine, SpaceBeforeParens switch (e) { case 1: return e; case 2: return 2; }; // AllowShortBlocksOnASingleLine, SpaceBeforeParens while (true) { } while (true) { continue; } } // AlignAfterOpenBracket, BinPackParameters, void loooonFunctionIsVeryLongButNotAsLongAsJavaTypeNames( std::vector<AAAAAAAAAAAAAAAAAAAA> const& inputVector, std::map<int, std::string>* outputMap) { std::vector<AAAAAAAAAAAAAAAAAAAA> bar; std::copy_if(inputVector.begin(), inputVector.end(), std::back_inserter(bar), &shortFilter); // AllowShortLambdasOnASingleLine std::sort(inputVector.begin(), inputVector.end(), [](auto v) { return v.a < v.b; }); std::transform( inputVector.begin(), inputVector.end(), std::inserter(*outputMap, outputMap->end()), [](const AAAAAAAAAAAAAAAAAAAA& element) { // LambdaBodyIndentation return std::make_pair(element.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, element.cccccccccccccccccc); }); }; int notInline(AAAAAAAAAAAAAAAAAAAA* v); }; // AllowShortFunctionsOnASingleLine int notInline(AAAAAAAAAAAAAAAAAAAA* v) { return v->a + 1; } } // namespace LevelTwoNamespace } // namespace LevelOneNamespace int main() { // ReflowComments // veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongComment // with plenty of information /* second * veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongComment * with plenty of information */ // SortUsingDeclarations using std::cin; using std::cout; int aaaaaaaaaaaaaaaaaaa, bbbbbbbbbbb, ppppppppppp, eeeee; // AlignConsecutiveAssignments aaaaaaaaaaaaaaaaaaa = 6; bbbbbbbbbbb = 5; ppppppppppp = 10; LevelOneNamespace::LevelTwoNamespace::B b{ 1, 3, 4, // SpaceBeforeCpp11BracedList std::vector<std::string>{"aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbb", "cccccccccccccccccccccccccccc"} }; // AllowShortLoopsOnASingleLine for (int i = 0; i < 10; i++) cout << i; LevelOneNamespace::LevelTwoNamespace::AAAAAAAAAAAAAAAAAAAA ddddddddddddddddddddddddd{5, 5, "ff"}; b.notInline(ddddddddddddddddddddddddd); // SpaceAfterCStyleCast, AllowAllArgumentsOnNextLine cout << (bool)b.myFunction(aaaaaaaaaaaaaaaaaaa, bbbbbbbbbbb, ppppppppppp, eeeee, 0); return 0; } ```