跳转至

C++ 語法基礎

代碼框架

如果你不想深究背後的原理,初學時可以直接將這個「框架」背下來:

1
2
3
4
5
6
7
#include <cstdio>
#include <iostream>

int main() {
  // do something...
  return 0;
}
什麼是 include?

#include 其實是一個預處理命令,意思為將一個文件「放」在這條語句處,被「放」的文件被稱為頭文件。也就是説,在編譯時,編譯器會「複製」頭文件 iostream 中的內容,「粘貼」到 #include <iostream> 這條語句處。這樣,你就可以使用 iostream 中提供的 std::cinstd::coutstd::endl 等對象了。

如果你學過 C 語言,你會發現目前我們接觸的 C++ 中的頭文件一般都不帶 .h 後綴,而那些 C 語言中的頭文件 xx.h 都變成了 cxx,如 stdio.h 變成了 cstdio。因為 C++ 為了和 C 保持兼容,都直接使用了 C 語言中的頭文件,為了區分 C++ 的頭文件和 C 的頭文件,使用了 c 前綴。

一般來説,應當根據你需要編寫的 C++ 程序的需要來確定你要 #include 哪些頭文件。但如果你 #include 了多餘的頭文件,只會增加編譯時間,幾乎不會對運行時間造成影響。目前我們只接觸到了 iostreamcstdio 兩個頭文件,如果你只需要 scanfprintf,就可以不用 #include <iostream>

可以 #include 自己寫的頭文件嗎?答案是,可以。

你可以自己寫一個頭文件,如:myheader.h。然後,將其放到和你的代碼相同的目錄裏,再 #include "myheader.h" 即可。需要注意的是,自定義的頭文件需要使用引號而非尖括號。當然,你也可以使用編譯命令 -I <header_file_path> 來告訴編譯器在哪找頭文件,就不需要將頭文件放到和代碼相同的目錄裏了。

什麼是 main()

可以理解為程序運行時就會執行 main() 中的代碼。

實際上,main 函數是由系統或外部程序調用的。如,你在命令行中調用了你的程序,也就是調用了你程序中的 main 函數(在此之前先完成了全局 變量 的構造)。

最後的 return 0; 表示程序運行成功。默認情況下,程序結束時返回 0 表示一切正常,否則返回值表示錯誤代碼(在 Windows 下這個錯誤代碼的十六進制可以通過 Windows Error Codes 網站 進行查詢)。這個值返回給誰呢?其實就是調用你寫的程序的系統或外部程序,它會在你的程序結束時接收到這個返回值。如果不寫 return 語句的話,程序正常結束默認返回值也是 0。

在 C 或 C++ 中,程序的返回值不為 0 會導致運行時錯誤(RE)。

註釋

在 C++ 代碼中,註釋有兩種寫法:

  1. 行內註釋

    // 開頭,行內位於其後的內容全部為註釋。

  2. 註釋塊

    /* 開頭,*/ 結尾,中間的內容全部為註釋,可以跨行。

註釋對程序運行沒有影響,可以用來解釋程序的意思,還可以在讓某段代碼不執行(但是依然保留在源文件裏)。

在工程開發中,註釋可以便於日後維護、他人閲讀。

在 OI 中,很少有人寫許多註釋,但註釋可以便於在寫代碼的時候理清思路,或者便於日後複習。而且,如果要寫題解、教程的話,適量的註釋可以便於讀者閲讀,理解代碼的意圖。希望各位同學能養成寫註釋的好習慣。

輸入與輸出

cincout

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
  int x, y;                          // 聲明變量
  std::cin >> x >> y;                // 讀入 x 和 y
  std::cout << y << std::endl << x;  // 輸出 y,換行,再輸出 x
  return 0;                          // 結束主函數
}
什麼是變量?

可以參考 變量 頁面。

什麼是 std

std 是 C++ 標準庫所使用的 命名空間。使用命名空間是為了避免重名。

關於命名空間的詳細知識,可以參考 命名空間 頁面。

scanfprintf

scanfprintf 其實是 C 語言提供的函數。大多數情況下,它們的速度比 cincout 更快,並且能夠方便地控制輸入輸出格式。

讀入輸出優化

cin/coutscanf/prinf 的具體差別和讀入輸出優化,請參考 讀入、輸出優化 頁面。

1
2
3
4
5
6
7
8
#include <cstdio>

int main() {
  int x, y;
  scanf("%d%d", &x, &y);   // 讀入 x 和 y
  printf("%d\n%d", y, x);  // 輸出 y,換行,再輸出 x
  return 0;
}

其中,%d 表示讀入/輸出的變量是一個有符號整型(int 型)的變量。

類似地:

  1. %s 表示字符串。
  2. %c 表示字符。
  3. %lf 表示雙精度浮點數 (double)。
  4. %lld 表示長整型 (long long)。根據系統不同,也可能是 %I64d
  5. %u 表示無符號整型 (unsigned int)。
  6. %llu 表示無符號長整型 (unsigned long long),也可能是 %I64u

除了類型標識符以外,還有一些控制格式的方式。許多都不常用,選取兩個常用的列舉如下:

  1. %1d 表示長度為 1 的整型。在讀入時,即使沒有空格也可以逐位讀入數字。在輸出時,若指定的長度大於數字的位數,就會在數字前用空格填充。若指定的長度小於數字的位數,就沒有效果。
  2. %.6lf,用於輸出,保留六位小數。

這兩種運算符的相應地方都可以填入其他數字,例如 %.3lf 表示保留三位小數。

「雙精度浮點數」,「長整型」是什麼

這些表示變量的類型。和上面一樣,會留到 變量 中統一講解。

為什麼 scanf 中有 & 運算符?

在這裏,& 實際上是取址運算符,返回的是變量在內存中的地址。而 scanf 接收的參數就是變量的地址。具體可能要在 指針 才能完全清楚地説明,現在只需要記下來就好了。

什麼是 \n

\n 是一種 轉義字符,表示換行。

轉義字符用來表示一些無法直接輸入的字符,如由於字符串字面量中無法換行而無法直接輸入的換行符,由於有特殊含義而無法輸入的引號,由於表示轉義字符而無法輸入的反斜槓。

常用的轉義字符有:

  1. \t 表示製表符。
  2. \\ 表示 \
  3. \" 表示 "
  4. \0 表示空字符,用來表示 C 風格字符串的結尾。
  5. \r 表示回車。Linux 中換行符為 \n,Windows 中換行符為 \r\n。在 OI 中,如果輸出需要換行,使用 \n 即可。但讀入時,如果使用逐字符讀入,可能會由於換行符造成一些問題,需要注意。例如,gets\n 作為字符串結尾,這時候如果換行符是 \r\n\r 就會留在字符串結尾。
  6. 特殊地,%% 表示 %,只能用在 printfscanf 中,在其他字符串字面量中只需要簡單使用 % 就好了。
什麼是字面量?

「字面量」是在代碼裏直接作為一個值的程序段,例如 3 就是一個 int 字面量,'c' 就是一個 char 字面量。我們上面寫的程序中的 "hello world" 也是一個字符串字面量。

不加解釋、毫無來由的字面量又被稱為「魔術數」(magic number),如果代碼需要被人閲讀的話,這是一種十分不被推薦的行為。

一些擴展內容

C++ 中的空白字符

在 C++ 中,所有空白字符(空格、製表符、換行),多個或是單個,都被視作是一樣的。(當然,引號中視作字符串的一部分的不算。)

因此,你可以自由地使用任何代碼風格(除了行內註釋、字符串字面量與預處理命令必須在單行內),例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* clang-format off */

#include <iostream>

 int 

    main(){
int/**/x, y;  std::cin
>> x >>y;
                std::cout <<
          y  <<std::endl   
     << x

          ;

    return       0;     }

當然,這麼做是不被推薦的。

一種也被廣泛使用但與 OI Wiki 要求的碼風不同的代碼風格:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* clang-format off */

#include <iostream>

int main()
{
    int x, y;

    std::cin >> x >> y;
    std::cout << y << std::endl << x;

    return 0;
}

#define 命令

#define 是一種預處理命令,用於定義宏,本質上是文本替換。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
#define n 233

// n 不是變量,而是編譯器會將代碼中所有 n 文本替換為 233,但是作為標識符一部分的
// n 的就不會被替換,如 fn 不會被替換成 f233,同樣,字符串內的也不會被替換

int main() {
  std::cout << n;  // 輸出 233
  return 0;
}
什麼是標識符?

標識符就是可以用作變量名的一組字符。例如,abcdabc1 都是合法的標識符,而 1ac+b 都不是合法的標識符。

標識符由英文字母、下劃線開頭,中間只允許出現英文字母、下劃線和數字。值得注意的是,關鍵字(如 int,for,if)不能用作標識符。

什麼是預處理命令?

預處理命令就是預處理器所接受的命令,用於對代碼進行初步的文本變換,比如 文件包含操作 #include 和 處理宏 #define 等,對 GCC 而言,默認不會保留預處理階段的輸出 .i 文件。可以用 -E 選項保留輸出文件。

宏可以帶參數,帶參數的宏可以像函數一樣使用:

1
2
3
4
5
6
7
#include <iostream>
#define sum(x, y) ((x) + (y))
#define square(x) ((x) * (x))

int main() {
  std::cout << sum(1, 2) << ' ' << 2 * sum(3, 5) << std::endl;  // 輸出 3 16
}

但是帶參數的宏和函數有區別。因為宏是文本替換,所以會引發許多問題。如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>
#define sum(x, y) x + y
// 這裏應當為 #define sum(x, y) ((x) + (y))
#define square(x) ((x) * (x))

int main() {
  std::cout << sum(1, 2) << ' ' << 2 * sum(3, 5) << std::endl;
  // 輸出為 3 11,因為 #define 是文本替換,後面的語句被替換為了 2 * 3 + 5
  int i = 1;
  std::cout << square(++i) << ' ' << i;
  // 輸出未定義,因為 ++i 被執行了兩遍
  // 而同一個語句中多次修改同一個變量是未定義行為(有例外)
}

使用 #define 是有風險的(由於 #define 作用域是整個程序,因此可能導致文本被意外地替換,需要使用 #undef 及時取消定義),因此應謹慎使用。較為推薦的做法是:使用 const 限定符聲明常量,使用函數代替宏。

但是,在 OI 中,#define 依然有用武之處(以下兩種是不被推薦的用法,會降低代碼的規範性):

  1. #define int long long+signed main()。通常用於避免忘記開 long long 導致的錯誤,或是調試時排除忘開 long long 導致錯誤的可能性。(也可能導致增大常數甚至 TLE,或者因為爆空間而 MLE)
  2. #define For(i, l, r) for (int i = (l); i <= (r); ++i)#define pb push_back#define mid ((l + r) / 2),用於減短代碼長度。

不過,#define 也有優點,比如結合 #ifdef 等預處理指令有奇效,比如:

1
2
3
4
5
#ifdef LINUX
// code for linux
#else
// code for other OS
#endif

可以在編譯的時候通過 -DLINUX 來控制編譯出的代碼,而無需修改源文件。這還有一個優點:通過 -DLINUX 編譯出的可執行文件裏並沒有其他操作系統的代碼,那些代碼在預處理的時候就已經被刪除了。

#define 還能使用 ### 運算符,極大地方便調試。