C++ 筆記

v0.36.12 - by 晴☆

憧憬

欸對,順便講一下我的目標,我想要一個

使用 JavaScript 開發 256 x 144, 2D 像素遊戲的平台

  • 只有 _init()、_update() 和 _draw() 的空腳本,可分頁(像 Pico-8)
  • 可用函式的 API 以及視覺化編輯器(像 Scratch 但仍是程式語言)
  • 強大的像素繪製工具(像 Blockbench)
  • 好用的音樂編輯器

預定要做但還沒做完(暫時寫在這)

  • 改基礎講義的順序!!!
  • STL 例題
  • 遞迴
  • 繼承與多型
  • 計算機
  • 一堆演算法

可用圖片

Slides.com

就是你現在正在看的簡報網站,按        吧

使用方法

  右下角的箭頭可以換頁,但簡報頁面的排列方式不是網格狀的,而是像很多串投影片,而它會記住使用者在每一串投影片的垂直位置(預設是最上方),所以先往下滑到底看完一個章節,再往右滑至另一個章節吧!

Title A
Title B
Title C
Title D
A1
B1
C1
D1
A2
C2

為什麼要用 

  • 線上方便共用
  • 編輯介面簡單
  • 支援數學嵌入
  • 支援程式嵌入
  • 很容易分章節
  • 看起來就很酷

小提示:

  • 也可以用鍵盤的方向鍵換頁

  • 按 [Esc] 可以看縮圖

  • 回到標題頁可以重置簡報順序

前言

做這份簡報的原因

我是誰

我是晴☆啊

  • 市井小民
  • 國小都在玩 Scratch
  • 國中時用 Lua 寫 Pico-8 遊戲
  • 會用 Node.js 跑程式碼
  • 從高一開始學 C++
  • 不喜歡 Python

製作這份簡報的原因

  • 這是我學習 C++ 的筆記
  • 可以當作社團成果
  • 高二社團若擔任幹部會需要
  • 我學習歷程沒東西放了

因為我高一上從無到有一點點學習 C++ 的過程中

覺得 C++ 挺有趣的

於是想記錄一下

這份簡報會以互動教學的角度,呈現我學 C++ 的歷程

備註與謝誌

備註:

  • 你好意思嗆我寫的爛?我又沒有收你錢

感謝:

  • 劉  三睪  羊原
  • 佑佑
  • 小海
  • 學長們的簡報
  • 社團同學們的意見
  • 說我寫很爛的 xelento

編輯器

總不能用記事本來寫程式吧

Microsoft Visual Studio

  編譯「簡單的輸入輸出程式碼」時可以省略一堆複雜的步驟,而且有自己的專案系統,做大型的專案時也很好用。當然也有另一種作專案的方法,可以用 G++ 自己編譯,但我覺得有點麻煩。

要勾選哪些功能

據我所知,現階段只要這個:

安裝清單應該會長這樣:

使用方法

首先,新增一個專案:

選擇這個模板:

最後,打上名字,按「建立」就完成了!

小提示

現在 VS 應該會是新細明體,很醜

可以在 工具 > 選項 > 環境 > 自型和色彩 自訂你的編輯介面!

字體列表中粗體字代表等寬

推薦試試 consolas 或 fixedsys 喔!

認識 C++

初始專案、編譯器與電腦內部

現在程式會長這樣:

// <檔案名稱>.cpp : 此檔案包含 'main' 函式。程式會於該處開始執行及結束執行。
//

#include <iostream>

int main()
{
    std::cout << "Hello World!\n";
}

// 執行程式: Ctrl + F5 或 [偵錯] > [啟動但不偵錯] 功能表
// 偵錯程式: F5 或 [偵錯] > [啟動偵錯] 功能表

// 開始使用的提示: 
//   1. 使用 [方案總管] 視窗,新增/管理檔案
//   2. 使用 [Team Explorer] 視窗,連線到原始檔控制
//   3. 使用 [輸出] 視窗,參閱組建輸出與其他訊息
//   4. 使用 [錯誤清單] 視窗,檢視錯誤
//   5. 前往 [專案] > [新增項目],建立新的程式碼檔案,或是前往 [專案] > [新增現有項目],將現有程式碼檔案新增至專案
//   6. 之後要再次開啟此專案時,請前往 [檔案] > [開啟] > [專案],然後選取 .sln 檔案

先執行看看

後會看到一個視窗跳出來:

解釋

這些是註解,純粹用來告訴別人(或者之後的自己)這裡在幹嘛

// 一行中,兩個斜線後面都是註解

/*
一整區塊的註解
很好用的
*/

真正會被執行的程式內容

#include <iostream>

int main()
{
    std::cout << "Hello World!\n";
}

程式的根本

大多數程序都是用高階語言編寫的
例如 C++、Java 或更高階的 Python

然而,由於電腦只理解數字,因此與計算機交談就像與一個語言不通的人交談
需要翻譯人員才能正確溝通,而這正是編譯器和直譯器所做的

人類語言使人與人之間的交流變得容易
而程式語言則是讓「命令電腦做工作」變得簡單

註:此段是翻譯自這篇文章的第一段

編譯 vs 直譯

編譯器和直譯器的區別,就是翻譯的時機

編譯器會在按下執行按鈕時,把所有程式碼翻譯成一些電腦的指令代碼

而直譯器則是一邊翻譯一邊執行

種類 編譯式語言 直譯式語言
優點 執行起來很快速 較容易檢查錯誤及修改
缺點 每次編譯都需要花很多時間 執行起來慢了一些

如何編譯

C++ 是編譯式語言,也就是說按下

 時,系統會把程式碼編譯成

基本上只有電腦才看得懂的「組合語言」,然後執行它。

那什麼是組合語言?

就是一些刻在主機板上的、執行起來非常快速的

只有超強開發者才看得懂的操作指令!

通常都是邏輯運算和記憶體操作

電腦怎麼跑,還是不知道啊

拆開電腦

電腦內部有什麼?

CPU 中央處理單位 處理一大堆有的沒的雜事,例如算數學
GPU 圖形處理單位 畫圖,例如在螢幕上畫一個正方形
記憶體 暫存資料 暫存的資料,例如正在看的網頁資料,關掉就沒了
硬碟 永久資料 永久的資料,例如儲存的照片、影片等等
輸入 輸入資料到 CPU 例如鍵盤、滑鼠等等
輸出 從 CPU 輸出資料 例如螢幕、揚聲器等等

接下來的程式碼大部分都在 CPU 做運算(基本上無圖形)並暫存資料在記憶體中

Online Judge

寫題目來進步!

網站

線上解題系統可以測試你的程度

分為很多題目,要根據不同輸入作相對應的輸出

一組輸入及輸出稱為測資 (測試資料)

跳過來講一下 Online Judge,常用的有:

怎麼用

  • 登入\(  /  \)註冊

  • 選個題目

  • ​按

  • 貼上程式碼

  • 看看有沒有通過,如果沒有的話要檢查程式碼是否有誤

  • 測試完,按

  • 查看結果

結果

簡稱 全名 白話的意思
AC Accept 通過
NA Not Accept 沒通過,有些是錯的
WA Wrong Answer 全錯
TLE Time Limit Exceeded 超時,程式太慢了
MLE Memory Limit Exceeded 沒有那麼多記憶體可以用
RE Runtime Error 執行到一半出現錯誤
CE Compile Error 編譯錯誤,請自己先試過再丟上來

程式語言

指令、函式庫與輸出

教電腦做事

這是一個簡單的範例:

起床;
吃飯;
睡覺;

不過這樣當然不會有效,因為電腦不知道「起床、吃、睡覺」到底是什麼

「起床、吃、睡覺」是指令,在 C++ 中每個指令後面要加上 ; 表示結束

所以接下來要開始講基礎語法了!

#include <iostream>

int main() {
    std::cout << "Hello World!\n";
}

先看到第 1 行

意思是:這個專案「包含」了 iostream 這個函式庫

而「函式庫」就是已經寫好,可以直接引用的功能

也就是別人已經寫好「怎麼輸入輸出」,只要使用就好了!

iostream 就是「input/output stream」,中文是「輸入/輸出流」,輸入輸出會用到!

接下來 3 到 5 行是在定義 main()

而 main() 之中就是會被執行的主程式

第 4 行就是「輸出 "Hello World!" 到主控台視窗」

剛才的程式碼

#include <iostream>

int main() {
    std::cout << "Hello World!\n";
}

標準函式庫 std

看到第四行,首先什麼是 std::cout 呢?
就是 std 這個「命名空間」裡面的 cout 這個東西
吃(薯條);
但電腦可能會以為是「吃肯德基的薯條」,所以要這樣寫:
吃(麥當勞::薯條);
想像一下,今天我跟電腦說「吃麥當勞的薯條」
所以「std 裡面的 cout」是什麼?
std 是標準函式庫,就是一些常用的模板
cout 的意思就是 console output (主控台輸出) 
std::cout << "Hello World!\n";

輸出

接下來要講那個 << 了
std::cout << "Hello World!\n";
簡單來說它就是把右邊的東西丟到左邊的意思
把 "Hello World!" 丟到 cout 就是你看到的輸出了!
註:它是製作者自己定義的功能,原本的 << 不是這個作用

輸入

答案是:不行

寫 iostream 的人沒幫你做這樣的功能

"Hello World!\n" >> std::cout;
那 >> 不就沒有用了?

你可能會好奇,輸出可不可以反過來寫?

當然有,將使用者輸入存入變數!

std::cin >> a;
// 等待使用者輸入,輸入什麼 a 就是什麼
[User] ------ input stream ---> [Computer]

[User] <---- output stream ---- [Computer]

「流」的概念

(之後會教變數怎麼用)

換行的 '\n'

"Hello World!\n" 最後面的 '\n' 沒有被印出來對吧
因為那是「Escape Sequence (跳脫序列)」,也就是特殊的字元!
最常用的:
'\n' 換行

如果你單純想放個反斜線呢?
'\\' 打兩個反斜線!
其他基本上不會用到

使用 using

剛才有「Namespace (命名空間)」概念了,但每次都打 std:: 手超痠的!
可以省略:
#include <iostream>

using namespace std;
// 以後沒有指定的話,就是用 std!

int main() {
    cout << "Hello World!\n";
}
若覺得這樣太籠統,也可以再分細一點:
using std::cin;
using std::cout;
// 以後 cin 和 cout 都是指 std 的!

註:若以專案開發來說,反而不要用 using 比較好

題目 zj d483

請輸出「hello, world」 (不含引號)

#include <iostream>
using namespace std;

int main() {
    cout << "hello, world\n";
    return 0;
}

注意:先自己試跑過再上傳,否則會像我:

if & else

條件

條件

判斷情況,做不同的事情

// 如果……就……否則就……
if (我想睡覺) {
    去睡覺;
}
else {
    打電動;
}

註:以上中文字都是範例,只是比較容易理解,並非正式寫法

// 如果……就……
if (我想睡覺) {
    去睡覺;
}

省略大括號

程式碼要力求簡潔!

如果大括號裡面只有一行,可以省略

怕會看不懂的話就不要省略

if (餓了) {
    吃東西;
}
else {
    睡覺;
}

// 可以這樣寫
if (餓了)
    吃東西;
else
    睡覺;
    
// 或者這樣也可以
if (餓了) 吃東西;
else 睡覺;

順帶一提,這種力求讓程式碼更容易閱讀的方式稱為「排版」

如果不排版的話其實所有程式碼也可以全部擠在同一行,完全看不懂

else if

else if 否則如果……就

它其實是長這樣:

if (...) {
    // do something
}
else {
    if (...) {
        // do something
    }
    else {
        // do something
    }
}

裡面的 if () {} else {} 算是一行,可省略大括號

if (...) {
    // do something
}
else 
    if (...) {
        // do something
    }
    else {
        // do something
    }
if (...) {
    // do something
}
else if (...) {
    // do something
}
else {
    // do something
}

因為太醜了

我們整理一下成為:

布林值

前面提到的條件
也就是放在 if () 裡的
叫做 boolean(布林值)
它其實只有兩種值:
true   =  1 (是)
false  =  0 (否)
if (true) {
    // 通過
}

if (1) {
    // 通過
}

if (87) {
    // 通過
}

if (-1) {
    // 通過
}

if (false) {
    // 不通過
}

if (0) {
    // 不通過
}

其實布林值是電腦記憶體的基本元素

像是常常會看到這種照片:

電腦中東西皆以二進位表示

所以其他所有「不是 0」的值

都會被判定為「true」

題目 zj a053

答對題數在 0~10 者,每題給6分。

題數在 11~20 者,從第11題開始,每題給2分。(前10題還是每題給6分)

題數在 21~40 者,從第21題開始,每題給1分。

題數在 40 以上者,一律100分。

變數

存放東西

變數 variable

宣告變數

int a;
// 宣告一個叫做 a 的 int,他的值沒定義,會是亂數。

unsigned int b = 10;
// 宣告一個叫做 b 的 unsigned int,他的值是 10。

char c = '!';
// 宣告一個叫做 c 的 char,他的值是 '!'。
bool wow = 0;
// 宣告一個叫做 wow 的 bool,他的值是 false

int a = 0;
// 宣告一個叫做 a 的 int,他的值是 0

a = 999;
// 現在 a 的值是 999 了

int a = 20;
// a 已存在,沒關係,一樣宣告 a 的值為 20

char a = '?';
// a 已經宣告過了,但他不是 char 而是 int,所以會爛掉


const int b = 0;
// 這邊用到了 const,意思是「常數」

b = 123;
// b 已被宣告為常數,不能修改

修改變數

宣告型別有:
bool      (boolean)  布林值       (是/否)
int       (integer)  整數
short                短整數           (範圍只有一半)
long                 就是整數      (完全就跟 int 一樣)
long long            超長整數      (範圍 2 倍大)
float                浮點數       (也就是小數)
double               雙倍精細的浮點數

以上如果加上 unsigned (例如 unsigned int) 
就只能存正數,而最大值會變成兩倍

char (charater) 字元
……其他

變數名稱和正負標記

變數名稱不能有空格,所以一般我們都分成兩種形式,看你喜歡哪個:
int how_much_money = 100; // 底線式

int howMuchMoney = 100;   // 駝峰式
其實沒有寫 unsigned 的都是 signed (有標記正負的),例如:
int n = -10;
// 完全等同於
signed int n = -10;
之後會提到關於 signed 和 unsigned 的原理

上限和下限

變數空間不是無限大的!

變數類型 別名(不用記) 位元組 範圍
short short int, __int16, int16_t 2 \( -{2}^{15} \leq x < {2}^{15} \)
int long, __int32, int32_t 4 \( -{2}^{31} \leq x < {2}^{31} \)
long long long long int, __int64, int64_t 8 \( -{2}^{63} \leq x < {2}^{63} \)
float 4 範圍詭異, 7 位小數
double 8 範圍詭異, 15 位小數
char __int8, int8_t 1 \( -{2}^{7} \leq x < {2}^{7} \)

註:兩個底線開頭的是非標準寫法(VS 才有)

上限和下限

變數空間不是無限大的!

變數類型 別名(只列出特殊的,不用記) 範圍
unsigned short uint16_t \( 0 \leq x < {2}^{16} \)
unsigned int uint32_t \( 0 \leq x < {2}^{32} \)
unsigned long long uint64_t \( 0 \leq x < {2}^{64} \)
unsigned char uint8_t \( 0 \leq x < {2}^{8} \)

註:兩個底線開頭的是非標準寫法(VS 才有)

註:標記成 unsigned 並不會改變占用的空間

字元

字元其實存的是它的編碼,是數字
稱作 Ascii Code

不要背!絕對不要背!完全沒有用!

只要記住 a~z、A~Z、0~9 是排在一起的
單引號 '' 裡面只能放一個字,代表字元
雙引號 "" 則可以放很多個字,代表字串(之後會講)

而字串就是很多個字元的「Array(陣列)」,之後會講到

型別轉換

char c = 'A';
// 前提:c 是 'A'

int(c)
// 65

static_cast<int>(c)
// 65

static_cast<bool>(c)
// true
型別(變數) 轉換成某個特定型別,必須要可以轉換
static_cast<型別>(變數) 同上

註:有些型別間不能轉換

但我一時舉不出已經教過的例子

輸入的方法

宣告一個變數後,可以從使用者輸入來改變他的值

int a;
cin >> a;

cout << "let's check the value of a:\n";
cout << a;

輸入會以空格和換行作為分隔,例如

int a;
int b;
int c;
int d;

cin >> a;
cin >> b;
cin >> c;
cin >> d;

輸入「3  4」的話,a = 3、b = 4

很多變數

很多時候,我們的程式碼中會有非常多東西,例如:

int grade_number_one   = 94;
int grade_number_two   = 87;
int grade_number_three = 96;
int grade_number_four  = 79;

但這樣建立一堆變數,顯然不是一個好的做法

這時要使用陣列 & 標準模板庫 STL 的好用模板

STL 也是標準函式庫底下的東西,有 vector, string, queue, stack 等等

之後會提到!

運算

聽起來就像是數學

運算子 (1)

運算子 意思 範例
+ 2 + 3 = 5
- 13 - 4 = 9
* 3 * 7 = 21
/ 8 / 4 = 2
% 取餘 27 % 8 = 3
&& 而且 true && false = false
|| 或者 true || false = true
! !true = false

範例

數字運算
int a = 1 + 3;
// a 是 4

int b = 12 + 8 / 4;
// b 是 14

int c = b - a;
// c 是 10

int d = b / a;
// 其實 b / a 是 3.5 但 d 被宣告為 int,
// d 會無條件捨去變成 3

float e = b / a;
// e 宣告為 float,是 3.5
邏輯運算
bool buy = false;
bool eat = true;

bool happy = buy || eat;
// eat 或者 buy,結果是 true

bool veryHappy = buy && eat;
// eat 而且 buy,結果是 false

// 還記得嗎,條件就是布林值
if (veryHappy) {

}

縮短

int a = 1;
// a 是 1

a = a + 3;
// a 是 4

// 不覺得「a = a +」很長嗎


int b = 1;
// b 是 1

b += 3
// b 是 4

// 變短了!
int a = 5;
// a 是 5

a += 1;
// a 是 6

// 不覺得「+= 1」如果常用的話,會懶得打嗎


int b = 5;
// b 是 5

b++
// b 是 6

// 變短了!

有「+=」、「-=」、「*=」、「/=」等等

有「++」和「--」兩種

int i, a;

i = 10;
a = ++i;
// i 先自己 +1,再把 a 賦值為 i
// 結果:a 是 11, i 也是 11

關於 ++ 和 --

其實它們可以寫在前面

但意思不太一樣

int i, a;

i = 10;
a = i++;
// 先把 a 賦值為 i,再把 i 自己 +1
// 結果:a 是 10, i 是 11

運算子 (2)

運算子 意思 範例 結果
== 等於 3 == 4 false
> 大於 3 > 2 true
< 小於 4 < 2 false
>= 大於等於 3 >= 3 true
<= 小於等於 2 <= 9 true
() 優先運算 (2 + 1) * 2 6
!= 不等於 5 != 8 true

注意 = 和 == 不一樣(很容易弄錯)

// 一個等於代表宣告他「左邊就是右邊」
// 也就是把右邊的值複製到左邊
int a = 3;
int b = 3;
int c = 5;

// 兩個等於代表詢問「是相等的嗎?」
if (a == b) {
    // 通過,這裡會被執行
}

if (a == c) {
    // 不通過,這裡不會被執行
}

括號

有的時候會出現一些很難看懂的東西,建議加上括號

bool isEqual = a == b;
// 看不太懂

bool isEqual = (a == b);
// 變數 isEqual 代表 a 是否等於 b
bool test = a >= b && c < d && e != 0;
// 看不太懂

bool test = ( (a >= b) && (c < d) && (e != 0) );
// 比較看得懂了吧,test 有三個條件

想知道運算元的優先順序可以看這裡

題目 zj a002

輸入兩個整數 a 和 b,輸出 a + b 的值

右邊是 AC Code

不過還可以更精簡

#include <iostream>
// 引入函式庫

using namespace std;
// 使用 std:: 命名空間

int main() {
    int a;
    int b;
    // 宣告 a 和 b 為 int
    
    cin >> a;
    cin >> b;
    // 輸入
    
    long long ans = a + b;
    cout << ans;
    // 輸出
    
    return 0;
    // 雖然 main() 不 return 也行,但最好加一下
}

注意事項:

a、b 範圍 \( 10^6 < 2^{31} \)
用 int 即可
a + b 範圍  \( |a + b| \) 可能會超過 \( 2^{31} \)
得用 long long 才行

更簡潔的方式

省略重複的步驟?

一次宣告很多變數

#include <iostream>
// 引入函式庫

using namespace std;
// 使用 std:: 命名空間

int main() {
    int a, b;
    // 宣告 a 和 b 為 int
    
    cin >> a >> b;
    // 輸入
    
    long long ans = a + b;
    cout << ans;
    // 輸出
    
    return 0;
    // 雖然 main() 不 return 也行,但最好加一下
    // 之後我都不會加,因為空間不夠了
}
int a = 0, b = 1, c = 2;
int d, e, f;

一次輸入很多變數

cin >> a >> b >> c;

為什麼輸入輸出的部分可以這樣寫?

輸入輸出運算子的原理

// 這是以 cin 為例,cout 則是反過來
cin >> a >> b;

(cin >> a) >> b;
// 電腦會先看到「cin >> a」於是先執行

(cin) >>  b
// 把使用者打的內容輸入進 a 以後,「cin >> a」會回傳一個「cin」

cin >> b
// 於是就會變成這樣

// 接著輸入 b 以後,又會回傳 cin...

cin
// 沒東西了,結束

超級快的 2 進位運算

位元運算

前情提要

這裡講得很難,只需要知道概念就好了。如果應付不來,可以跳過沒關係

另一方面,想知道更多的話可以點這裡

二進位概念:

\(0 + 1 = 1\)

\(1 + 1 = 「2」,進位變成  10\)

\(10 + 1 = 11\)

\(11 + 1 = 100\)

進位制轉換

十進位轉二進位:用二的次方取餘數,以 75 為例:
75 = 64 + 11
11 =  8 +  3
 3 =  2 +  1
 1 =  1
0 1 0 0 0 0 0 0
0 1 0 0 1 0 0 0
0 1 0 0 1 0 1 0
128 64 32 16 8 4 2 1
0 1 0 0 1 0 1 1

二進位轉十進位:每一位乘上 \( 2^n \) 再加起來,

0 1 0 0 1 0 1 1
128 64 32 16 8 4 2 1
 = 64 + 8 + 2 + 1 = 75
以 1001011 為例:

記憶體中的存放方式

剛才講變數的上限和下限時,都寫 \( a \leq x < b \) 

為什麼一邊是 \( \leq \) 而另一邊是 \( < \) 呢?

 

電腦中有一些空間用來存放變數值,假設是像這樣:

0 0 0 0 0

這樣是 \( 0 \)

0 0 4 0 1

這樣是 \( 401 \)

9 9 9 9 9

這樣是 \( 99999,也就是  10^5 - 1 \)

\( 99999 \) 再 + \( 1 \) 呢?

0 0 0 0 0

爆掉,變回 \( 0 \)

也就是說,「5 格」能存的數字為 \( 0 \leq x < {10}^{5} \)

但剛才是「假設」他這樣存,因為實際上電腦中是 2 進位的:

0 0 0 0 0 0 0 1

這樣是 \( 1 \)

0 0 0 0 1 1 0 1

這樣是 \( 13 \)

1 1 1 1 1 1 1 1

這樣是 \( 255,也就是  2^8 - 1 \)

\( 11111111 \)+ \( 1 \) 呢?

0 0 0 0 0 0 0 0

爆掉,變回 \( 0 \)

每一格是一個 Bit (位元),每 8 個 Bit 稱為一個 Byte (位元組)
所以 8 位元 ( 1 位元組) 能存的數字為 \( 0 \leq x < {2}^{8} \)

128 64 32 16 8 4 2 1

可是剛才沒有講到負數
接下來要講 signed!

「signed」就是「標記正負」,會使用最左邊的 1 個位元來標記這個數的正負

0 0 0 0 1 1 0 1

這樣還是 \( 13 \)

0 1 1 1 1 1 1 1

這樣是 \( 127,也就是  2^7 - 1 \)

1 0 0 0 0 0 0 0

這樣是 \( -128 \),最左邊的位元為 1,代表是負的

\( 01111111 \)+ \( 1 \) 呢?

-128 64 32 16 8 4 2 1

(跟 unsigned 一樣,但注意最左邊的位元是 \( -128\) )

1 0 0 0 0 0 0 1

再 + \( 1 \) 是 \( -127 \)

表示二進位

使用 0b 前綴表示二進位數字,例如:

cout << 0b1010;
// 8 + 2 = 10

但是 int 的輸出自動是 10 進位的

如果想要看二進位輸出就要用 bitset:

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    bitset<8> s = 75;
    // 8 位元的 bitset
    cout << s;
    // 01001011
}

NOT

直接看範例!

bitset<8> a = 0b00000101;
cout << "  A  - " <<  a  << "\n";
cout << " ~A  - " << ~a  << "\n";
  A  - 00110101
 ~A  - 11001010

就是把每個位元都變成相反

對記憶體中的每個位元進行邏輯運算就是位元運算的根本

兩個東西比對的邏輯

在看程式之前,先看看示意圖

AND [&]
OR [|]
XOR [^]
兩個都有
任一個是
只有其中一個是
0 & 0 = 0
1 & 0 = 0
0 & 1 = 0
1 & 1 = 1
0 & 0 = 0
1 & 0 = 1
0 & 1 = 1
1 & 1 = 1
0 ^ 0 = 0
1 ^ 0 = 1
0 ^ 1 = 1
1 ^ 1 = 0

AND OR XOR

把兩個物件(位元數必須相同!)每個位元拿去做邏輯比對

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    bitset<8> a = 0b11110001;
    bitset<8> b = 0b01011011;

    bitset<8> f = a & b;

    cout << "  A   - " <<  a   << "\n";
    cout << "  B   - " <<  b   << "\n";
    cout << "----------------" << "\n";
    cout << "A & B - " <<  f   << "\n";
}
  A   - 11110001
  B   - 01011011
----------------
A & B - 01010001

用途

寫「x & 1」可以判別基偶數
因為與 1 做 AND 就是「最後一位是不是 1」的意思
0 1 0 1 0 1 0 1
0 0 0 0 0 0 0 1
-128 64 32 16 8 4 2 1
x:
1:
&
0 0 0 0 0 0 0 1
true

左移 & 右移

在使用 cout 時會打的「<<」其實就是左移的意思

看看例子就懂了:

int main() {
    bitset<8> a = 0b00000101;
    cout << " A      - " <<  a       << "\n";
    cout << " A << 1 - " << (a << 1) << "\n";
    cout << " A << 2 - " << (a << 2) << "\n";
    cout << " A << 3 - " << (a << 3) << "\n";
    cout << " A << 4 - " << (a << 4) << "\n";
}
 A      - 00000101
 A << 1 - 00001010
 A << 2 - 00010100
 A << 3 - 00101000
 A << 4 - 01010000

左移就是乘以 0b10 的意思,也就是 \(\times2\),相對的,右移則是 \(\div2\)

神之編譯器

講了那麼多,其實如同我一開始所說的,知道概念就好了,不會學也沒差!

因為 C++ 編譯器會自動找到最快的方法,像是把 \(\times2\)  轉換成 << \(1\) 等等

所以,程式中完全不需要寫出位元運算!

迴圈

簡化重複的事。while & for loop, break & continue

while 迴圈

寫法是這樣:

// while 迴圈:當……的時候就……
while (生氣) {
    吃東西;
    打電動;
}

註:以上中文字都是範例,只是比較容易理解,並非正式寫法

所以當生氣時

就會一直吃東西、打電動、吃東西、打電動……

直到不生氣為止

break

註:以上中文字都是範例,只是比較容易理解,並非正式寫法

while (生氣) {
    吃東西;
    
    if (吃太飽) {
        break;
        // 跳脫出迴圈
    }
}
// 跳脫到這裡

迴圈可以用 break 跳脫出來

但只會跳脫一層

// while 迴圈:當……的時候就……
while (想吃壽司) {
    走進迴轉壽司;
    
    while (還沒飽) {
        拿壽司來吃;
        
        if (有點太貴了) {
            break;
            // 這裡會跳脫
        }
    }
    // 會跳脫到這裡
    休息一下;
}

for 迴圈

模組化的 while 迴圈

int i = 1;
while (i <= 20) {
    cout << i << " ";
    
    i++;
}
// 輸出 1 2 3 ... 18 19 20
// For 迴圈:從……開始,當……的時候,每次做……事
for (int i = 1; i <= 20; ++i) {
    cout << i << " ";
}

想像一個情況:

使用 for 迴圈!

continue

for (int i = 1; i <= 20; ++i) {
    // 如果是 3 的倍數就跳過
    if (i % 3 == 0) {
        continue;
    }
    
    cout << i << " ";;
}
// 跳脫到這裡

迴圈可以用 continue 跳過一次

題目 zj a147

輸入一個整數 \(n  (n \leq 10000\))

若 n = 0 表示資料結束

輸出所有符合下列條件的數字

  • 大於 0
  • 整數
  • 不可以被 7 整除
  • 小於 n

注意事項:

n 範圍 \( 10^5 < 2^{31} \), 用 int 即可
// 從……開始,當……的時候,每次做……事
for (int i = 1; i < n; ++i) {
    // 如果 i 除以 7 的餘數不是 0
    if (i % 7 != 0)
        cout << i << " ";
}
// 完整程式碼

#include <iostream>
using namespace std;

int main() {
    for (int i = 1; i < n; ++i) {
        if (i % 7 != 0)
            cout << i << " ";
    }
    cout << "\n"
}

可以使用 for 迴圈:

優化

判斷餘數 (假設是 3)

if (i % 3 != 0) {}

看看 i % 3 的可能性

0 1 2

解析為布林值

if (i % 3) {}
false true true

所以可以寫成

順便講一下 IO 優化

在文字被輸出前,會先到一個「緩衝區」

等到特定時間點才會一次輸出,這樣比較快

而 cin 會強制將緩衝區的資料釋放,才不會把使用者輸入的字跟輸出的字搞混

但這在題目並不需要,多一步驟反而會佔時間

所以我們可以在 main() 的一開始就取消它:

ios_base::sync_with_stdio(false);
cin.tie(0);

建議在上傳程式碼前再加上去,否則自己會很困擾

函數

傳入傳出

函數 function

定義一個函數

根據傳入的數值,回傳出結果

// 這樣寫
int myFunction(int a, int b) {
    int answer = a + b;
    
    return answer;
}

// 解釋
回傳型別 函數名字(型別1 傳入物件1, 型別2 傳入物件2……) {
    // 要做的事情
    
    return 要回傳的值;
}

呼叫一個已經定義好的函數

傳入一些數值,就會回傳出結果

int a = myFunction(20, 30);
// 宣告 a 為 myFunction(20, 30) 的結果,也就是 50

int b;
b = myFunction(20, 30);
// 這樣可以

int c = myFunction(30, 40) + myFunction(80, 90);
// 這樣也可以

注意:函數需要先被宣告,後面的程式碼才能呼叫它!

預設值

傳入的值可以有預設值,會自動填滿

#include <iostream>
using namespace std;

int myFunction(int a = 10, int b = 0) {
    int answer = a + b;
    return answer;
}

int main() {
    cout << myFunction(3, 4) << " "
         << myFunction(3)    << " "
         << myFunction()     << "\n";
}
7 3 10

若沒傳入 a 則 a = 10,若沒傳入 b 則 b = 10

void 和 ()

函數不一定要傳入值,也不一定要回傳值

可以用在簡略重複的事情

// 使用 void (虛空) 表示不回傳值,括號內留空表示不傳入值
void sunday() {
    if (goToCramSchool) {
        happyValue -= 10;
    }
    else {
        happyValue += 5;
    }
}

//別的地方可以呼叫 sunday
sunday()

改良 if

三原運算子、標籤與跳躍、switch & case

三原運算子

// 概念
條件 ? 如果通過的話 : 如果沒有的話
// 打招呼的情境
cout << (isCloseFriend ? "yo! bro!" : "hello!");

程式碼要力求簡潔

所以如果只有一個 else 可以寫成三原運算子

a ? b : c

// 想像它是一個函數
if (a) {
    return b;
}
else {
    return c;
}

標籤 & 跳躍

有概念就可以了,絕對不要這樣寫!

這樣寫沒有任何好處

而且會讓程式碼超混亂

所以概念是什麼?

標記一個位置,可以跳躍到那裡

#include <iostream>
using namespace std;

int main() {
    int num;

start:
    cout << "here is the start!\n";

middle:
    cout << "here is the middle!\n";

    cin >> num;

    if (num == 1) {
        goto start;
    }
    else if (num == 2) {
        goto middle;
    }
}
// 標記程式碼的這個地方
標籤:

// 回到標籤的位置繼續執行
goto 標籤;

switch

switch (東西) {

case 第一種情況:
    // do something
    break;
    
case 第二種情況:
    // do something
    break;

// 不一定要有 default
default:
    // do something
    break;
}
int num;
cin >> num;

switch (num) {

case 1:
    cout << "one!\n";
    break;
    
case 2:
    cout << "two!\n";
    break;
    
case 3:
    cout << "three!\n";
    break;
    
default:
    cout << "idk...\n";
    break;
}

程式碼要力求簡潔

所以如果有很多的 else if

可以寫成 switch & case

舉個例子:

case & default

仔細看,case 沒有換行,而且每個 case 都有 break!

因為它是標記,也就是說

電腦看到 switch 以後,把 () 裡面的東西拿去比較

跳躍相對應的 case 位置,開始執行,直到有 break 才跳脫出 switch

這樣設計是有它的原理:

  • 可以不同情況做同樣的事

  • 可以分成多組

(雖然很少會出現這些情況)

switch (num) {
case 1:
case 4:
case 7:
    cout << "good!\n";
    break;
default:
    cout << "i don't like it...\n";
    break;
}

題目 zj a058

輸入一個正整數 n

代表接下來有 n 個數

輸出接下來 n 個數中

\( 3k,  3k+1,  3k+2  (k \in \N ) \)

各有幾個

#include<iostream>
using namespace std;

int main() {
    int n;
    cin >> n;

    int zero = 0, one = 0, two = 0;

    int k;
    for (int i = 0; i < n; ++i) {
        cin >> k;

        switch (k % 3) {
        case 0:
            zero += 1;
            break;
        case 1:
            one += 1;
            break;
        case 2:
            two += 1;
            break;
        }
    }

    cout << zero << " " << one << " " << two << "\n";
}

簡單題,右邊是 AC Code

如何判斷?

使用 switch & case!

排版

超級重要

排版技巧

排版不會影響程式執行,但會使程式碼比較容易閱讀。

排版並沒有絕對的好壞,也會依照不同人的習慣而改變

但最根本的規則就是要「全部一致」。

// 壞的排版示範

#include <iostream>
int main()
{
int a=  0;
if (a == 0) {std::cout 
  << "hello";
    };return 0;
    }
// 好的排版示範

#include <iostream>

int main() {
    int a = 0;
    
    if (a == 0) {
    	std::cout << "hello";
    }
    
    return 0;
}

晴☆的排版方式

有些人的大括號 {} 前會換行,也可以

再次重申,排版會依照個人習慣而改變

#include <iostream>

using namespace std;

int main() {
    int a = 0;
    
    if (a != 0) {
    	cout << "wow! the value of a is "
             << a << " and it's so cool!";
    }
    
    return 0;
}
  • 小括號和大括號中間空一格
  • 運算子前後空一格
  • 不同層的程式碼以四個空格作為縮排
  • 能清楚判斷對應的括號在哪
  • 不同段落間空一行
  • 一行寫不下的東西,換行再加一層縮排
    (相似的東西盡量對齊)
array & std::vector

陣列們

陣列 array

8 2 3 5
內容物型別 名稱[大小] = { 內容物…… };
int arr[4] = { 8, 2, 3, 5 };
[0]
[1]
[2]
[3]

編號(索引值)是「和第一個東西的距離」!

First Item

電腦記憶體中的樣子

把很多東西排成一排儲存,編上編號,寫法是:

注意:陣列大小只能放常數,不能放變數

Size

\( 0 < 編號 \leq \) size

也可以用不初始化的宣告:

內容物型別 名稱[大小];
int arr[4];

自動判定大小:

取得某一項的值:

int arr[4] = { 8, 3, 4, -5 };

int n = arr[2];
// n 的值設為 arr 的第 2 項,也就是 4
int arr[] = {0, 1, 2};
// 大小自動填入 3 了

可變陣列 std::vector

雖然陣列是一個很基本的東西,但它很難用,完全可以被 std::ector 取代

其實 Vector 在數學中是「向量」,但在 C++ 中卻是改良版陣列

改良版陣列跟向量的關係是?沒有關係,純粹同名。

#include <vector>

記得引用 vector 函式庫

vector<內容物型別> 名稱 = { 內容物…… };
vector<int> v = { 8, 2, 3, 5 };

不用指定大小!

也可以用不初始化的宣告:

vector<內容物型別> 名稱;
vector<int> v;

全部初始化成同一值:

vector<內容物型別> 名稱(大小, 初始值);
vector<int> v(100, 0);
// 0 ~ 99 項都是 0

「取得某一項的值」與陣列的方法一樣:

vector<int> vec = { 8, 3, 4, -5 };

int n = vec[2];
// n 的值設為 vec 的第 2 項,也就是 4

大小怎麼辦

既然 vector 不用指定大小,因為它能屈能伸、可大可小!

vector<int> vec = { 8, 6, 4 };

vec.push_back(2);
// vec 尾端新增「2」,變成 { 8, 6, 4, 2 }

cout << vec.size();
// 輸出現在 vec 的大小,為 4

vec.pop_back();
// vec 尾端刪除一項,變成 { 8, 6, 4 }

for (int i = 0; i < vec.size(); ++i) {
    cout << vec[i] << " ";
}
// 輸出 vec 的每一項,以空格分隔

關於方法

為什麼是 vec.push_back(n) 而不是 push_back(vec, n) 呢?

因為 push_back(n) 的結果只會關係到 vector 自己,是 vector 的方法

std::vector::size() 是一個函數,如果把它丟在 For 迴圈「當……時」那一格

它就會每執行一次都呼叫一次,等待回傳值,會有點慢!

for (int i = 0, s = vec.size(); i < s; ++i) {
    cout << vec[i];
}
// 把 s 在「從……開始」那一格宣告成 vec 的 size,之後只要讀變數 s 的值,就會比較快!

(之後講到 struct 和 class 時會詳細說明)

char[] & std::string

字串們

字串

'a' 'p' 'p' 'l' 'e' '\0'
char 名稱[大小] = "字串……";
char name[6] = "apple";
[0]
[1]
[2]
[3]

First Item

其實就是字元的陣列,寫法是:

Length

[4]

我知道 "apple" 是五個字,但最後面會有一個隱形的 '\0' 代表字串結束

所以它的字串長度理論上是 5 但 size 卻是 6!

Size

[5]

改良字串 std::string

雖然字串是一個很基本的東西,但它很詭異,所以我們用 std::string 取代它

其實 String 在英文中本來就是「字串」的意思!

#include <string>

記得引用 string 函式庫

string 名稱 = { 內容物…… };
string favoriteFruit = "apple";

不用指定大小!

註:有興趣的話,可以查一下 string 與 vector<char> 的差別

同樣可自由伸縮

string str = "apple";

str.push_back('!');
// str 尾端新增「!」,變成 "apple!"

cout << str.length();
// 輸出現在 str 的長度,為 6

str.pop_back();
// str 尾端刪除一項,變成 "apple"

str.insert(3, "...");
// str 從第 4 項插入「...」,變成 "app...le"

cout << str;
// 輸出 str
queue, deque & priority_queue (std)

隊列們

(+ #define)

隊列 std::queue

先進先出,沒有索引號

可以想像是在排隊

試試看

#include <queue>

別忘了引用 queue 函式庫

queue<int> q;
// 一個嶄新的 queue

q.push(1);
// 尾端加入 1
q.push(2);
// 尾端加入 2 變成 1 2
q.push(3);
// 加入 3 變成 1 2 3

cout << q.front() << "\n";
// 輸出最前面,是 1
cout << q.back() << "\n";
// 輸出最後面,是 3

q.pop();
// 前端刪除一項,變成 2 3
cout << q.front() << "\n";
// 現在最前面的是 2
// 把東西都刪掉
while (q.size() > 0) {
    q.pop()
}

// 或者可以寫
while (!q.empty()) {}

// 或者這樣,因為不是 0 都會被判定為 true
while (q.size()) {}

std::deque

其實是雙頭的 queue,沒什麼好解釋的:

看 VS 的提示:

優先隊列 std::priority_queue

顧名思義,是有優先順序、會自己排序的 queue 啦

priority_queue<int> q;
// 一個嶄新的 PQ

q.push(3);
q.push(5);
q.push(4);

cout << q.top();
// 排序後取得最優先的(這裡是最大的)值,是 5

(但是它其實是 heap)

排序方式

可以變更排序方式!

priority_queue<內容物型別, 儲存方式, 比較方式>

priority_queue<int, vector<int>, less<int>>
// 由大排到小,top() 是最大的

priority_queue<int, vector<int>, greator<int>>
// 由小排到大,top() 是最小的

另外也可以自訂排序方式,但是有點進階又很少用

想知道的可參考這篇文章

縮短名字的方法

template<typename T, typename C = vector<T>, typename P = less<T>>
using pq = priority_queue<T, C, P>;
#define pq priority_queue
用 #define A B 的意思就是用 A 取代程式碼中所有的 B

更好的方式是「template + using」,但是這種寫法有點進階,之後才會教

std::stack

堆疊

堆疊 std::stack

先進先出,沒有索引號

可以想像是在疊一疊書

使用方法

#include <stack>

嗯,記得引用 stack 函式庫

stack<int> stk;
// 一個嶄新的 stack

stk.push(1);
stk.push(2);
stk.push(3);
// 現在是 1 2 3

cout << stk.top();
// 最上面是 3

stk.pop();

cout << stk.top();
// 最上面是 2

std::set

集合

哭啊 SET

想寫的東西越來越多了

std::map

關聯式容器

哭啊 MAP

想寫的東西越來越多了

多維陣列

陣列裡有陣列

(+ 中斷點 & EOF)

兩層 for 迴圈

我們看看這個例子:

for (int i = 1; i <= 3; ++i) {
    // 注意這裡不能再用 j 了
    for (int j = 1; j <= 4; ++j) {
        // i-j
        cout << i << "-" << j << " ";
    }
    cout << "\n";
}
1-1 1-2 1-3 1-4
2-1 2-2 2-3 2-4
3-1 3-2 3-3 3-4

外面的 for 迴圈是每一行

裡面的 for 迴圈是每一行中的每一項

存進陣列

又……燒雞了……

題目 zj a015

輸入一個矩陣的行數、列數與內容

輸出「行與列相反」的矩陣

3 1 2
8 5 4

輸入:

2 3 (行數 列數)
3 8
1 5
2 4

輸出:

註:還是別忘了測資範圍啊!

如何儲存矩陣?

多維的陣列!

也就是陣列裡有陣列!


 

 
3 1 2
8 5 4

(原本題目給的矩陣歪掉了,哈哈)

輸入矩陣

#include <iostream>
#include <vector>
using namespace std;

int main() {
    // 幾行、幾列
    int numRows, numColumns;
    cin >> numRows >> numColumns;

    // 暫時宣告一個空的行
    vector<int> tempRow(numColumns, 0);

    // 是 vector 的 vector,並將每行填入剛才的空行
    vector<vector<int>> vec(numRows, tempRow);

    for (int r = 0; r < numRows; ++r) {
        for (int c = 0; c < numColumns; ++c) {
            // 輸入內容
            cin >> vec[r][c];
        }
    }

    return 0;
}

輸入順序會長這樣:

1 2
3 4 5 6 7
8 9 a b c
d e f g h
i j k l m

所以我們的 for 迴圈要設定好:

for (/* 跑過每一行,要放行數 */) {
    for (/* 跑過一行中的每一項,要放列數 */) {
        // 此時輸入
        // 順序就會是正確的
    }
}

中斷點

如何知道輸入是不是正確的?

暫停程式,看變數的值!

在 VS 中,把滑鼠移動到剛才的 Code 第 23 行的數字左邊

就會有一個圈圈,點下去以後會變成紅色的

此時執行程式,就會在 23 行停下來!

查看變數的值

既然程式暫停了

我們可以看看程式碼有沒有問題

這種行為叫做 debug

回到 VS,左下角應該有:

點開 vec 即可看到裡面有什麼!

可以看到 vec[0] (第一行)是 { 3, 1, 2 }

(請忽略那個 [capacity] 和 [allocator],那是內建的複雜東西)

輸出矩陣

再宣告一個旋轉後的矩陣然後輸出?

那樣很佔空間又很慢!

其實只要輸出時改順序就好了

// 輸出!注意,這邊是「列在外、行在內」

// 把每直列當作每行
for (int c = 0; c < numColumns; ++c) {
    // 輸出原本直列中的每一項
    for (int r = 0; r < numRows; ++r) {
        cout << vec[r][c] << " ";
    }
    cout << "\n";
}
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int numRows, numColumns;
    cin >> numRows >> numColumns;

    vector<int> tempRow(numColumns, 0);
    vector<vector<int>> vec(numRows, tempRow);

    for (int r = 0; r < numRows; ++r) {
        for (int c = 0; c < numColumns; ++c) {
            cin >> vec[r][c];
        }
    }

    for (int c = 0; c < numColumns; ++c) {
        for (int r = 0; r < numRows; ++r) {
            cout << vec[r][c] << " ";
        }
        cout << "\n";
    }
}

右邊是我的 Code,但它不是 AC 解!

EOF

這題有很多矩陣在同一組測資

如何判斷結束?

那什麼是 EOF?

就是未完成的輸入

在 windows 系統中

就是按 Ctrl + Z 然後按 Enter

(應該會顯示 "^Z" )

寫法:

int num;

// 比較推薦的寫法
// 不會有問題,而且可以用 getline (之後會教)
while (true) {
    cin >> num;
    if (cin.eof()) break;
    
    // do something
}

// 也可以這樣寫
// 因為 cin 如果出現 eof 會回傳 0
while (cin >> num) {
    // do something
}

許多題目以輸入 0 表示結束

但這題是到 EOF 為止!

正解

#include <iostream>
#include <vector>
using namespace std;

int main() {
    while (true) {
        int numRows, numColumns;
        cin >> numRows >> numColumns;
        if (cin.eof()) break;

        vector<int> tempRow(numColumns, 0);
        vector<vector<int>> vec(numRows, tempRow);

        for (int r = 0; r < numRows; ++r) {
            for (int c = 0; c < numColumns; ++c) {
                cin >> vec[r][c];
            }
        }

        for (int c = 0; c < numColumns; ++c) {
            for (int r = 0; r < numRows; ++r) {
                cout << vec[r][c] << " ";
            }
            cout << "\n";
        }
    }
}

這邊還要加題目!

晴☆燒雞啦!

遞迴

stack overflow 出現了

工作堆疊 work stack

電腦需要處理的事情會是一個 stack,裡層先處理

#include <iostream>
using std::cout;

void b() {
    cout << "-----> B start & end\n";
}

void a() {
    cout << "--> A start\n";
    b();
    cout << "--> A end\n";
}

int main() {
    cout << "MAIN start\n";
    a();
    cout << "MAIN end\n";
}
MAIN start
--> A start
-----> B start & end
--> A end
MAIN end

main()

a()

b()

遞迴概念

#include <iostream>
#include <string>
using namespace std;

void test(int num) {
    cout << num << " ";
    test(num - 1);
}

int main() {
    test(30);
}

函數呼叫自己!

上方的程式碼中,test(num) 會輸出 num 並呼叫 test(num - 1)

執行看看就會發現它輸出 30 29 28…… 到 -3000 多的時候就出錯了!

堆疊溢位 stack overflow

因為不斷呼叫自己就會一直堆疊,最終超過限制,堆疊溢位了!

需要最終有一層停止呼叫自己才能停止!

main()

test(30)

test(29)

test(28)

#include <iostream>
#include <string>
using namespace std;

void test(int num) {
    cout << num << " ";

    if (num <= 0) return;

    test(num - 1);
}

int main() {
    test(30);
    // 30 29 28 ... 2 1 0
}

程式論壇 stack overflow

其實有一個網站就叫做 stack overflow 欸

它是一個有非常多資訊的程式論壇

可以在上面問問題,或者搜尋別人問過的問題

如果有關於程式的問題,找 stack overflow 就對了!

(它的 icon 很有 stack overflow 的感覺吧!)

題目 zj e357

指著一個變數

記憶體位置、指標、變數範圍與參考

變數的位置

用 & 取得變數存放的位置:

int test;
cout << &test << "\n";
// 記憶體位置每次分配不會一樣
000000DB38EFF8F4

我跑了一次程式碼,得到:

跑過陣列試試看:

我再跑一次程式碼,得到:

000000C65A10FB24
int arr[10];
for (int i = 0; i < 10; ++i)
    cout << &arr[i] << "\n";
00000046FBF1FBB8
00000046FBF1FBBC
00000046FBF1FBC0
00000046FBF1FBC4
00000046FBF1FBC8
00000046FBF1FBCC
00000046FBF1FBD0
00000046FBF1FBD4
00000046FBF1FBD8
00000046FBF1FBDC

排在一起,每項 4 位元組:

000000279D8FFBF4

我跑第三次程式碼,得到:

進位方式

剛才那串看似數字,但又混入英文字母的是什麼呢?

是 16 進位!

由 0 ~ f 組成,所以 9 + 1 會是 a,f + 1 才是 10

以 0x 作為前綴表示 16 進位數字:

int n = 0xd;
// n 在 16 進位中是 d
cout << n;
// 以 10 進位輸出,是 13

之前提過二進位是以 0b 作為前綴:

int n = 0b11;
// n 在 2 進位中是 11
cout << n;
// 以 10 進位輸出,是 3

指標 pointer

指標簡單來講就是:指著一個變數

address of n: 00000048C9EFF764
  value of p: 00000048C9EFF764
int n = 3;
// 一個變數 n ,它的值是 3

int* p = &n;
// 一個指著 n 的指標 p,它的值是「n 的位置」

cout << "address of n: " << &n << "\n"
     << "  value of p: " <<  p << "\n";

結果:

更動 *p,就是更動 n!

int n = 3;
// 一個變數 n ,它的值是 3

int* p = &n;
// 一個指著 n 的指標 p,它的值是「n 的位置」

cout << "before: " << n << "\n";
*p += 1;
cout << " after: " << n << "\n";

結果:

before: 3
 after: 4

參考 reference

 表示 a 是參考 b 的變數

int& a = b

使用

int a = 77;
int& ref_a = a;

cout << ref_a << "\n";
// 77

ref_a = 20;
cout << a << "\n";
// 20

也就是說,a 完全就是 b!

註:參考一定要有初始值

名字、值 & 址

名字 (name) 我們可以將變數命名,讓我們比較好理解
值 (value) 存放的「內容」,也是最重要的部分
址 (address) 存放的「位置」,是變數的首位位元組的編號

你可能會好奇,變數型態需要儲存嗎?

答案是不用。

因為在編譯時轉換成組合語言,可以想像它什麼都不知道,只是執行命令操作記憶體,所以我們必須確保程式不會出錯,否則就沒救了!

複雜的型別

int 32 位元整數
int* 指向 int 的指標
int& 指向 int 的參考
需要注意的是:
「int*」是一種型別,代表指向 int 的指標;「*x」是取得指標 x 指向的變數
「int&」是一種型別,代表指向 int 的參考;「&x」是取得變數 x 的址

很容易搞混!

來製作 swap 吧

假設今天你需要把兩個變數的值互換

int a = 10, b = 3;

int temp = a;
a = b;
b = temp;

那我們寫成函數?

#include <iostream>
using namespace std;

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    return;
}

int main() {
    int x = 4, y = 1;
    swap(x, y);
    // 呼叫 swap,執行 swap 裡面的程式碼
    cout << "x: " << x << "\n" 
         << "y: " << y << "\n";
}

原理就是:

  • 把 a 存到「暫存變數」
  • 把 a 變成 b 的值
  • 把 b 變成「暫存變數」的值,也就是 a 原本的值

試試看,就會發現它沒有用!

變數範圍 scope

為什麼剛才的 swap 函數沒有用呢?

這就要談到變數的作用範圍了

在把 x 和 y 傳入時,就像這樣:

int a = x, b = y;

int temp = a;
a = b;
b = temp;

所以 a 和 b 是存在另外的位置,改變 a 和 b 不會連動到 x 和 y!

main()

swap()

傳入值

外面

回傳值

x   y

a   b

函數傳入參考

傳入值 (call by value):

in main(): 000000AA36BCFB34
in test(): 000000AA36BCFB10
#include <iostream>
using namespace std;

void test(int a) {
    cout << "in test(): " << &a << "\n";
}

int main() {
    int t;
    cout << "in main(): " << &t << "\n";
    test(t);
}

使用參考 (call by reference):

in main(): 00000045BDF2F784
in test(): 00000045BDF2F784
#include <iostream>
using namespace std;

void test(int& a) {
    cout << "in test(): " << &a << "\n";
}

int main() {
    int t;
    cout << "in main(): " << &t << "\n";
    test(t);
}

用途

來看看正確的 swap 吧!

順便說說 reference 的用途

  • 當需要在函數內變更函數外的變數

  • 當保證不會更動到函數傳入的變數

前者顯而易見,而後者是因為比較快!

畢竟少做了「複製一份」的動作

#include <iostream>
using namespace std;

void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
    return;
}

int main() {
    int x = 4, y = 1;
    swap(x, y);
    // 呼叫 swap,執行 swap 裡面的程式碼
    cout << "x: " << x << "\n" 
         << "y: " << y << "\n";
}

最後要注意的是,使用參考不能傳入非變數的值,例如這邊的 swap()

如果呼叫 swap(a, 3) 的話,編譯器會出錯:const int 不能轉換為 int&

函數分身

在 C++ 中,函數可以根據不同的「傳入值」做不同的處理,例如:

#include <iostream>
#include <string>
using namespace std;

void test(int n) {
    // 傳入的是整數!
    cout << "Wow! this is a integer: " << n << "\n";
}

void test(string str) {
    // 傳入的是字串!
    cout << "Wow! this is a string:  " << str << "\n";
}

int main() {
    test(95);
    test("hey!");
}
Wow! this is a integer: 95
Wow! this is a string:  hey!

物件 & 分類

Struct, Class & Namespace

一個人與他的屬性

假設這裡有個人叫做 Jack,他 180 公分高、80 公斤重

string        person_1_name    =  "Jack";
unsigned int  person_1_height  =  180;
unsigned int  person_1_weight  =  80;
double        person_1_BMI
    = person_1_weight / pow(static_cast<double>(person_1_height) / 100, 2);

// 註:pow 是 <cmath> 中的函數
// pow(a, b) 就是 a 的 b 次方

這樣有四個變數,而且名字還很長……

這時,我們需要一個叫做「人」的物件!

結構體 Struct

#include <iostream>
#include <string>
#include <cmath>
using namespace std;

struct person {
    string        name;
    unsigned int  height;
    unsigned int  weight;
    double        BMI;
}; // <-- 不要忘記分號(其實 VS 會幫你加)

int main() {
    person one;
    one.name = "Jack";
    cout << one.name;
}
Jack

把一堆變數包在一起,變成一個新的自訂型別!

建構式 constructor

struct person {
    string        name    =  "unknown";
    unsigned int  height  =  150;
    unsigned int  weight  =  50;
    double        BMI     =  0;

    person(string n_, unsigned int h_, unsigned int w_) {
        name   = n_;
        height = h_;
        weight = w_;
        BMI    = weight / pow(static_cast<double>(height) / 100, 2);
    }
};

這個 person() 不是一個用來呼叫的函數,而是建構一個 person 時會做的事!

int main() {
    person one("Jack", 180, 80);
    cout << one.BMI;
}
24.6914

解構式 destructor

跟建構式相反,是被移除時要做的事(雖然目前不需要)

struct person {
    string        name    =  "unknown";
    unsigned int  height  =  150;
    unsigned int  weight  =  50;
    double        BMI     =  0;

    ~person(string n_, unsigned int h_, unsigned int w_) {
        cout << "just remove a person";
    }
};
just remove a person
int main() {
    person one;
    return 0;
}

結果:

如果 main() 裡面這樣寫:

person one("Ann", 160, 50);
one.weight = 100;
cout << one.BMI;

體重變成 100,BMI 卻仍是 19.5312!

因為 BMI 只有在被宣告的時候計算

改變體重並沒有連動到 BMI!

反而是這樣改會有用……

person one("Ann", 160, 50);
one.BMI = 999;
cout << one.BMI;

出問題了

註:程式出問題時稱為 bug

想知道典故點此

寫成方法

#include <iostream>
#include <string>
#include <cmath>
using namespace std;

struct person {
    string        name;
    unsigned int  height;
    unsigned int  weight;
    double        BMI;

    person(string n_, unsigned int h_, unsigned int w_) {
        name = n_;
        height = h_;
        weight = w_;
        calculateBMI();
    }

    void calculateBMI() {
        BMI = weight / pow(static_cast<double>(height) / 100, 2);
    }
};

int main() {
    person one("Ann", 160, 50);
    one.weight = 100;
    one.calculateBMI();
    cout << one.BMI;
}
39.0625

寫一個方法計算 BMI!

結果:

但是這樣就要每次都計算一次,很麻煩!

(物件底下的函數稱為方法)

換個做法

#include <iostream>
#include <string>
#include <cmath>
using namespace std;

struct person {
    string        name;
    unsigned int  height;
    unsigned int  weight;
    double        BMI;

    person(string n_, unsigned int h_, unsigned int w_) {
        name = n_;
        height = h_;
        weight = w_;
        calculateBMI();
    }

    // 在改變體重時計算 BMI (理論上身高也要,但是我這邊沒有做)
    void setWeight(unsigned int w_) {
        weight = w_;
        BMI = weight / pow(static_cast<double>(height) / 100, 2);
    }
};

int main() {
    person one("Ann", 160, 50);
    one.setWeight(100);
    cout << one.BMI;
}

寫在外面

在 C++ 中,函數及方法可以先在正確的位置宣告後

在其它地方實作

struct myStruct {
    string name = "unknown";
    
    void setName(string n_);
};

// 這裡實作 myStruct 的 setName()
void myStruct::setName(string n_) {
	name = n_;
}

函數:

int test(string a);

int main() {
    // 因為宣告過了,所以可以使用
    test("nothing :D")
}

// 傳入的值必須和宣告時相同
int test(string a) {
    cout << "wow! it's " << a;
}

方法:

類別 class

就是進階版 struct 啦

class myClass {
public:
    void   changeName(string n_);
    string getName();
private:
    string name;
};

void myClass::changeName(string n_) {
    name = n_;
}

string myClass::getName() {
    return name;
}
public 外部可以存取的東西
private 只有自己可以存取的東西

好處:

  • 避免修改到一些會連動的東西
  • 看起來很帥

像是 string 和 vector 其實都是 class,但 vector 還有用到模板,非常複雜

命名空間 namespace

之前有提到過,命名空間就是用來區分同樣名稱的不同功能

#include <iostream>
#include <string>
using namespace std;

namespace myNamespace {
    void chatMessage(string name, string content);
    void joinBroadcast(string name);
};

void myNamespace::chatMessage(string name, string content) {
    cout << "<" << name << "> " << content << "\n";
}

void myNamespace::joinBroadcast(string name) {
    cout << name << " joined the game\n";
}

int main() {
    myNamespace::joinBroadcast("Jack");
    myNamespace::chatMessage("Jack", "Hello Everyone!");
}
Jack joined the game
<Jack> Hello Everyone!

計算機

太長了,只好當一個章節來講

但是只是計畫要寫

畢竟進度還差很遠

還沒結束

之後再繼續做囉!

C++ 筆記

By 晴☆

C++ 筆記

  • 177