C++ Notes on The Cherno Videos

1. Welcome to C++

2. How to Setup C++ on Windows

3. How to Setup C++ on Mac

4. How to Setup C++ on Linux

5. How C++ Works

  • int main() 可以不用 return 0 。只有 main() 可以這樣

  • << operator 也是一個函數

  • 一個 cpp 檔裡只要所有用到的變數函數都找的到宣告,compiler 就相信它存在,實際定義在哪是 linker 要負責找到。

  • 在同一個 cpp file 裡只要所有函數變數都找的宣告就可以編譯。不需要另外 include

  • 把 text 編譯成 binary file 的三個步驟:

    • preprocess: h -> cpp

    • compile: cpp -> obj

    • link: obj files -> exe

  • platform & configuration :

    • platform 如 x86 是 target platform(x86==win32)

    • configuration 如 Debug/Release 是 build config

    • 檢查跟 property 裡的一樣。VS 有時候會弄錯

6. How the C++ Compiler Works

  • preprocessor 只負責複製貼上。把 } 改成 #include "EndBrace.h" 結果是一樣的

  • *.i file 是 preprocess 過的 cpp file

  • compile 是 compile cpp files individually 的意思,在 windows 裡編譯完得 obj 檔被 VS 丟到 /Debug 裡

  • C++ 沒有 file 的概念,只有 translation unit :

    • 用 include 把 a.cpp 貼到 b.cpp,再把 b.cpp 貼到 c.cpp,最後只編譯 c.cpp,這樣就只有一個很大的 translation unit

  • compile 得到的 obj file 是 binary。要得到可讀的 assembly code 可以在 VS 改:

    • Porperty -> C/C++ -> Output Files -> Assumbler Output 改成 Assembling only listing

    • 改好之後重新編譯,除了 obj 還會得到 asm file 是可讀的

  • O2 編譯是優化速度。還可以優化別的:

    • 下面這兩行會被 O2 會編成 return a*b,在 assembly 省下一個 mov

[2]:
int f(int a, int b)
{
    int c = a*b;
    return c;
}
  • 因為 O2 不是一行一行編譯所以沒辦法 debug

  • Constant Folding:

    • return 5*2; 會直接編譯成 return 10; 就算在 debug mode 也一樣

    • 所有編譯時期能決定的 constant 就會直接算出結果放在 assembly

7. How the C++ Linker Works

  • 一個函數只要在某個檔案定義過了 linker 就找的到,和 include 無關,include 只需要 include 宣告

  • linker 找的是 function signature

  • 一個程式的 entry point 不一定就是 main function,雖然大部份時候都是

  • 分清楚 error 是 compiling(error code C 開頭)還是 linking error(LNK 開頭)

  • 兩種常見 linking error:

    • unresolved external symbol:有變數或函數找不到定義

    • one or more multiple defined symbols found:有變數或函數重覆定義,有可能是 preprocessor 貼兩次的結果,solution:

      • 把函數宣告成 static,只有那個 translation unit 看的到

      • 宣告成 inline

      • 不要把函數定義在 h 檔裡,而是定義在 cpp 檔自成一個 translation unit

      • 除了第一次定義之外,其它都用 extern,如 extern int a;

  • linker 也用來 link 其它 library 如 STL,platform API

  • linking 有分 static 和 dynamic

8. Variables in C++

Type

Size (Bytes)

bool

1

char

1

int

4

double

8

  • * 代表 compiler dependent

  • 可以用 sizeof 來查

  • C++ 所有 primitive type 其實都是數字,差別只在暫記憶體空間大小

  • bool 其實只需要一個 bit,可是 memory 都是以 byte 為單位讀,所以就用 1 byte 來存

  • float 宣告會自動變成 double 除非宣告成這樣 float a = 5.5f;

9. Functions in C++

  • 常常複製貼上一段 code 之後忘了改一些細節。如果一段 code 會被複製貼上很多次就該包成函數

  • 練習把 code 拆成很多很多函數

  • 但不要 over do it 因為有 context switch overhead 會變慢。function 的 assembly code 在 binary 的其它位置

10. C++ Header Files

  • 如果沒有 header file,每一個 translation unit 都要把自己用到的所有函數重新宣告一次。有 header file 就可以用 include 的就好

  • 在一個 header file 的最前面放 #pragma once 可以避免這個 header file 被重覆 include,效果同把整個 header file 的內容包在 ifndef 裡:

[ ]:
#ifndef _LOG_H_
#define _LOG_H_
#endif
  • 現在已經差不多所有編譯器都支援 #pragma once

  • #include <header>#include "header.h" 的差別:

    • 編譯的時候可以指定一些 include path,用括號的 include 是從這些 path 中找,用雙引號的 include 是用該 cpp file 的相對位置找

    • #include "header.h" 代表這個 header 和 cpp 在同一個 folder

    • 其實雙引號也會去 include path 找所以 #include "iostream" 是可以的

  • C++ 自帶的 header file 是沒有副檔名的,所以不能 #include <iostream.h>,會找不到。但是 C 自帶的是有的,例如 #include <math.h>

11. How to DEBUG C++ in VISUAL STUDIO

12. CONDITIONS and BRANCHES in C++ (if statements)

13. BEST Visual Studio Setup for C++ Projects!

14. Loops in C++ (for loops, while loops)

  • for(i=0 ; i<=4 ; i++) 會比 for(i=0 ; i<5 ; i++) 慢因為 i<=4 其實是兩次比較。只要情況允許應該永遠寫 i<5

15. Control Flow in C++ (continue, break, return)

16. POINTERS in C++

  • 不管是 int*, char*, double* 都一樣是整數

  • 以下三個都相同:

[13]:
{
    void* ptr1 = nullptr;
    void* ptr2 = NULL;
    void* ptr3 = 0;
}
[16]:
{
    int var = 8;
    double* ptr = (double*)&var;
}
  • cstring memset 把一塊連續的記憶體設成同一個值

[15]:
# include <cstring>
{
    char* buffer = new char[8];
    std::memset(buffer, 0, 8);
    delete[] buffer;
}

17. REFERENCES in C++

  • 函數傳參考呼叫的寫法

  • 宣告參考的時候一定要初始化(沒有本名哪有別名)(it’s not a real variable, it’s a reference)

  • 一旦初始化就不能改變。下面這段 code 只會把 a 的值設成 8,不會把 ref 變成 b 的別名

[17]:
{
    int a = 5;
    int b = 8;
    int& ref = a;
    ref = b;
}

18. CLASSES in C++

19. CLASSES vs STRUCTS in C++

  • 差別真的就只有 struct 預設是 public,class 預設是 private

  • The Cherno 遇到真的只用來裝 data 的才用 struct,然後從來不繼承 struct

20. How to Write a C++ Class

21. Static in C++

  • static 分成在 class 裡的和在 class 外的,意義不同,這裡講在 class 外的

  • external linkage:在不同 translation unit 裡重覆定義非 static 變數會出現 linking error。要避免這個錯誤可以寫 extern int a; 叫 linker 去其它 translation unit 找

    • extern int a; 是宣告而非定義,見 Stroustrup p.430

    • extern 和(class 外的)static 是用來控制 linker 的行為

  • internal linkage:在一個 translation unit 的全域範圍裡的 static 只有該 translation unit 看的到。linker 從外部找不到,就算用 extern 也找不到。這個變數就像是這個 translation unit 的 private 變數

  • static 函數也一樣,只有該 translation unit 看的到。這樣定義:static void Function(){}

  • 如果在標頭檔裡定義 static 變數然後這個標頭檔被兩個 cpp file include,由於 preprocessor 只是單純的複製貼上,這相當於在兩個 translation unit 裡各自定義 static 變數

  • 就像 class 裡的儘量把外部用不到的變數函數定義成 private 一樣,全域變數也應該儘量定義成 static,除非真的需要它被 linker 找到

22. Static for Classes and Structs in C++

  • 在 class 裡的 static variable 只有一個 instance。不管宣告多少這個 class 的 instances 都共用同一個該 variable。然後在全域要用 scope resolution operator 再宣告一次

[ ]:
#include <iostream>

// won't run in xeus-cling but perfectly legal in VS

class Entity
{
public:
    static int a;
    static void Print()
    {
        std::cout << a << std::endl;
    }
};

int Entity::a;

int main()
{
    Entity e;
    e.a;
    e.Print();
}
  • 上面的 e.a 是合法的編譯會過,但觀念正確的呼叫方式應該是 Entity::a

  • static 成員變數可以是 private,所以比全域變數更好控制權限

  • 非 static 的所有 class 成員函數經過編譯其實都有一個像 python 的 self 把呼叫自己的物件傳進去,static 成員函數沒有,所以如果要存取其它成員變數,只能存取 static 成員變數。沒有 self 就沒辦法透過 self 物件存取裡面的變數

23. Local Static in C++

  • 在一個 local scope(如函數,if statement)裡的 static 變數 lifetime 會變成永遠,但 scope 不變

[4]:
#include <iostream>

void Function()
{
    static int i = 0;
    i++;
    std::cout << i << std::endl;
}
  • 上面這段 code 有點像把 int i = 0; 移到全域。如果呼叫這個函數五次,i 就會被遞增五次(所以可以計算這個函數被呼叫了多少次)

    • 上面這段 code 的初始化 i = 0 只會做一次,因為第二次以後呼叫這個函數時 i 已經存在了不需要初始化

  • 那為什麼不用全域變數就好了?因為 local static 變數不會改變 scope,只有這個函數能計算自己被呼叫的次數。如果宣告成全域變數,在呼叫五次的過程中可能有其它 code 會改變 i 的值

  • 兩種 Singleton 的寫法,行為完全一樣,用 local static 寫比較乾淨

[ ]:
class Singleton
{
private:
    static Singleton* s_Instance;
public:
    static Singleton& Get() { return *s_Instance; }
    void Hello() {}
};

Singleton* Singleton::s_Instance = nullptr;

int main()
{
    Singleton::Get().Hellow();
}
[ ]:
class Singleton
{
public:
    static Singleton& Get()
    {
        static Singleton instance;
        return instance;
    }
    void Hello() {}
};

int main()
{
    Singleton::Get().Hellow();
}
  • 上面第二種寫法裡的 static Singleton instance; 只有第一次被呼叫時真正 new 了一個 Singleton,之後就一直用同一個 instance

24. ENUMS in C++

  • 幫整數取名字,增加可讀性

[2]:
{
    enum Example : unsigned char  // 如果不指定預設為 int。不可以用 float 因為一定要整數
    {
        A=0, B=2, C=6  // 如果不指定而只寫 A, B, C 預設為 0, 1, 2
    };

    Example value = B;
}
  • Log class 例子:

[4]:
#include <iostream>

class Log
{
public:
    enum Level
    {
        LevelError=0, LevelWarning, LevelInfo  // 這樣初始化可以增加可讀性。代表 0, 1, 2
    };
private:
    Level m_LogLevel = LevelInfo;
public:
    void SetLevel(Level level)
    {
        m_LogLevel = level;
    }
    void Error(const char* message)
    {
        if (m_LogLevel >= LevelError)
            std::cout << "[ERROR]: " << message << std::endl;
    }
    void Warn(const char* message)
    {
        if (m_LogLevel >= LevelWarning)
            std::cout << "[WARNING]: " << message << std::endl;
    }
    void Info(const char* message)
    {
        if (m_LogLevel >= LevelInfo)
            std::cout << "[INFO]: " << message << std::endl;
    }
};

Log log;
log.SetLevel(Log::LevelInfo);  // scope resolution operator
log.Warn("Hello!");
log.SetLevel(Log::LevelError);
log.Warn("Hello in level error");

[WARNING]: Hello!
  • 如果 m_LogLevel 是 int,有可能會不小心被改成 5。宣告成 Level 會比較安全

  • SetLevel 裡用 scope resolution operator Log::LevelError 就像 LevelError 是一個成員變數一樣。enum Level 裡並不是一個 namespace 而是有一個 enum class

    • 所以這個例子裡 enum 變數不能叫 Error,會跟成員函數重名

    • 像這個例子的 enum 裡變數名前面灌上該 enum 的名稱也是 common practice

25. Constructors in C++

26. Destructors in C++

27. Inheritance in C++

28. Virtual Functions in C++

  • 父類別指標可以指向子類別物件,然後通過 arrow operator 呼叫子類別版本的 virtual 成員函數

[ ]:
#include <iostream>

// won't run in cling but perfectly legal in VS

class Entity
{
public:
    virtual std::string GetName() { return "Entity"; }
};

class Player : public Entity
{
private:
    std::string m_Name;
public:
    Player(const std::string& name) : m_Name() {}
    std::string GetName() override { return m_Name; }
};

void PrintName(Entity* entity)
{
    std::cout << entity->GetName() << std::endl;
}

Player* p = new Player("Cherno");
PrintName(p)

  • 這裡被傳進 PrintName 的指標實際上是指向子類物件還是父類物件,編譯時期是不知道的

  • C++11 之後子類裡的 override 成員函數定義裡可以加上 override 關鍵字,不是強制的但

    1. 可以增加可讀性

    2. 可以避免函數名字打錯(如果打成 GETNAME() override 編譯會出錯)

    3. 如果父類裡忘了宣告這個函數為 virtual 編譯也會出錯

  • overhead

    1. 父類裡多出 4 bytes 用來存指向子類 vtable 的指標

    2. 到了子類要查 vtable 找到想要的函數

Vtable

  • 如果沒有任何 virtual function,sizeof 一個類別就是所有成員變數總和(成員函數不佔空間)。一旦出現 virtual function 而成為一個父類,sizeof 會多出 4 bytes 用來存指向子類 vtable 的指標

  • 每一個子類有一個自己的 vtable,同一個子類的所有 instance 都共用這個 vtable

  • vtable 裡有很多函數指標,指向所有在這個子類裡有 override 的 virtual function

  • vtable 簡介在 Stroustrup 的 The C++ Programming Language, 4th Ed 第三章(p.67)

29. Interfaces in C++ (Pure Virtual Functions)

  • 在父類裡把某個成員函數的 body 拿掉換成 =0;,這個父類就變成 interface,不能有自己的 object,並且強迫所有子類要 override 這個函數

[ ]:
virtual std::string GetName() = 0;

30. Visibility in C++

31. Arrays in C++

  • Lua array index 從 1 開始

  • 如果存到超出 array 的範圍,在 debug mode 會 crash 但在 release mode 不會有任何警告,然後就會很難 debug

  • pointer arithmetic: main 裡面的三行做的事一樣

[5]:
int main()
{
    int a[5];
    int* ptr = a;

    a[2] = 5;
    *(ptr + 2) = 5;
    *(int*)((char*)ptr + 8) = 5;

    return 0;
}
  • ptr + 2 實際上在記憶體裡要 offset 幾個 byte 取決於 ptr 是哪一類的指標。int 佔 4 bytes,char 只佔 1 byte,所以如果把 ptr cast 成 char* 之後要加 8 才會指到同一個位址(然後還要 cast 回來)

  • stack 和 heap based array 的差別

    1. lifetime:stack based 只看 scope,heap based 就是 new 出來的,要一直到看到 delete 才會消失

    2. new 比較慢因為會有 memory fragmentation,cache misses

  • C++11 之後有 std::array。儘量用 std::array 取代 raw array

[4]:
#include <array>

int main()
{
    std::array<int, 3> a = {1, 2, 3};
}
  • stack based array 例如 int a[5]; 可以這樣算出長度 sizeof(a)/size(int),heap based 就沒辦法了(其實有辦法但是 compiler dependent,不要用)

  • 用一個變數來 manage size:

[3]:
{
    static const int size = 5;
    int a[size];
}
  • 這裡一定要用 static const 因為 stack based array 長度必需在編譯時期就知道(實測不用?甚至直接 int size = 5; 也行)。如果是 heap based 就不用

32. How Strings Work in C++ (and how to use them)

33. String Literals in C++

34. CONST in C++

  • int* const a = new int; 不能改指向別的位址,但可以改裡面住的人(從右往左讀:a 是一個 const)

  • const int* a = new int; 可以改指向別的位址,但不能改裡面住的人(從右往左讀:a 是一個指標,指向 const int)

  • int const* a = new int; 同上。只有 const 放在 * 前後會有差別。總結:

    1. 只有直接接在 const 後面的東西才是真正不能改

    2. int*int* 要分開來看

    3. int const = const int

  • const int* const a = new int; 位址和內容都不能改

  • const 成員函數,不能改任何成員變數

[6]:
class Entity
{
private:
    int m_x, m_y;
    mutable int val;
public:
    int GetX() const
    {
        val = 2;    // 宣告成 mutable 的成員變數可以改
        return m_x;
    }
};
  • const int* const GetPtr() const 回傳一個指標,位址和內容都不能改,並且這個成員函數也不更改任何成員變數

  • 下面這個函數裡 e 只能呼叫 const 成員函數,因為傳進來的時候就被限定是 const ref,不能改 e 裡面的任何東西。所以有時候會看到同樣的成員函數被定義兩次,一次是 const 另一次不是

[7]:
#include <iostream>

void PrintEntity(const Entity& e)
{
    std::cout << e.GetX() << std::endl;
}

35. The Mutable Keyword in C++

  • mutable 關鍵字有兩個不同的意義:

    1. 使某成員變數在 const 成員函數裡能被改寫,看上面的例子。大部份時候是為了 debug,如果直接把該成員函數改成 non-const 可能會 break 別的東西

    2. lambda(The Cherno 實務上從來沒看過人這麼寫,從來不需要)

      • auto f = [=]() 把 scope 裡所有東西傳進去,by value

      • auto f = [&]() 把 scope 裡所有東西傳進去,by ref

[1]:
#include <iostream>

int main()
{
    int x = 8;
    auto f = [=]() mutable
    {
        x++;  // call by value 的時候如果不把這個 lambda 函數宣告成 mutable,就不能改內容
        std::cout << x << std::endl;
    };
    f();
    // x 還是 8,並沒有被遞增,因為 call by value
}

36. Member Initializer Lists in C++ (Constructor Initializer List)

37. Ternary Operators in C++ (Conditional Assignment)

38. How to CREATE/INSTANTIATE OBJECTS in C++

39. The NEW Keyword in C++

40. Implicit Conversion and the Explicit Keyword in C++

  • Implicit Conversion (Construction):像下面的例子,當有對應的 ctor 時可以直接 initialize 成 Entity a = 22;

    • C#, Java 沒有

  • 雖然合法但不要寫 Entity a6 = 22;,不好讀

[1]:
#include <iostream>
#include <string>

class Entity
{
private:
    std::string m_Name;
    int m_Age;
public:
    Entity(const std::string& name)
        : m_Name(name), m_Age(-1) {}

    Entity(int age)
        : m_Name("Unknown"), m_Age(age) {}
};

{
    // normal initialization
    Entity a1("Cherno");
    Entity a2(22);
    Entity a3 = Entity("Cherno");
    Entity a4 = Entity(22);

    // implicit conversion
    Entity a6 = 22;
    // Entity a5 = "Cherno";    // For some reason The Cherno 可以這樣寫但在 cling 不行
}
  • compiler 只允許做一次 implicit conversion。下面的例子裡 PrintEntity("Cherno"); 企圖先把 "Cherno" (7 個字元的 const char array) convert 成 const std::string 再 convert 成 Entity,做不了兩次 conversion 會報錯

[ ]:
void PrintEntity(const Entity& entity){
    // Printing
}

{
    PrintEntity(22);
    // PrintEntity("Cherno");  // 不合法,"Cherno" 是 const char [7]
    PrintEntity(std::string("Cherno"));
}
  • 如果 ctor 宣告成 explict,implicit conversion 就會 disable,Entity a6 = 22; 也會報錯

  • 但是可以 cast:Entity a5 = (Entity)22;

  • explicit key word 不常用,重要的是知道 complier 是有做 implicit conversion 的

[ ]:
#include <iostream>
#include <string>

class Entity
{
private:
    std::string m_Name;
    int m_Age;
public:
    explicit Entity(const std::string& name)
        : m_Name(name), m_Age(-1) {}

    explicit Entity(int age)
        : m_Name("Unknown"), m_Age(age) {}
};

{
    // normal initialization
    Entity a1("Cherno");
    Entity a2(22);
    Entity a3 = Entity("Cherno");
    Entity a4 = Entity(22);
    Entity a5 = (Entity)22;    // casting

    // implicit conversion disable, 下面三個都會報錯
    // Entity a6 = 22;
    // Entity a7 = "Cherno";
    // PrintEntity(std::string("Cherno"));
}

41. OPERATORS and OPERATOR OVERLOADING in C++

42. The “this” keyword in C++

43. Object Lifetime in C++ (Stack/Scope Lifetimes)

  • 直接宣告變數獲得的記憶體空間都是 stack based

    • 每一層 scope 都是 stack 上的一個 frame。每新建一個 scope 就是在 push,就像把一本書放到一整疊書的最上面。當在這個 scope 裡宣告變數就等於是把它寫在這本書裡。離開這個 scope 就是 pop,把這本書連同所有內容全部丟掉

  • 用 new 配置的記憶體空間是 heap based,在 delete 之前這塊空間是不會消失的

  • 常見錯誤:在函數裡配置 stack based 記憶體空間然後回傳指標,這樣做一出函數時配置給 a 的記憶體空間就消失了

[ ]:
int* CreateArray()
{
    int array[50];
    return array;
}

int main()
{
    int* a = CreateArray();
}
  • Scoped class:寫一個 class 當作 python 的 with 來用。用 ctor 和 dtor 來當作 __enter____exit__,可以用來寫:

    • timer

    • mutex locking for multithreading

    • scoped pointer,如 unique pointer:

[ ]:
class ScopedPtr
{
private:
    Entity* m_Ptr;
public:
    ScopedPtr(Entity* ptr): m_Ptr(ptr){}
    ~ScopedPtr()
    {
        delete m_Ptr;
    }
};

int main()
{
    {
        ScopedPtr e = new Entity();
    }
}

44. SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)

  • new 完不用 delete,在 out of scope 時記憶體會自動被釋放(物件毀滅)

  • 試著用 smart pointers 取代 raw pointers。能用 unique_ptr 就用 unique_ptr,真正需要 share 的時候才用 shared_ptr

unique_ptr

  • 不能被複製。因為如果 ptr1 = ptr2,後來 ptr1 先 go out of scope 了,所指向的物件就會毀滅,繼續使用 ptr2 就會出錯(或者當 ptr2 也 go out of scope 也會出錯)

[ ]:
#include <memory>

int main()
{
    {
        // std::unique_ptr<Entity> e1 = new Entity();  不合法因為 unique_ptr 的 ctor 是 explicit
        std::unique_ptr<Entity> e1(new Entity());
        std::unique_ptr<Entity> e2 = std::make_unique<Entity>();
        // std::unique_ptr<Entity> e3 = e2;  不合法因為不能複製。unique_ptr 的 copy ctor 和 assignment 都被設成 delete 了
        e1 -> Print();
        e2 -> Print();
    }
}
  • 呼叫 Entity 成員的方式完全同 raw pointer

  • 一出 scope 物件自動毀滅

  • 用 make_unique 作初始化比較安全,處理了 constructor 出 exception 的情況

  • 幾乎沒有 overhead

shared_ptr

  • 有一個 reference counting system 計算這個 ptr 被複製幾次,變成零的時候釋放記憶體

[ ]:
#include <memory>

int main()
{
    {
        std::shared_ptr<Entity> e1(new Entity());  //是合法的但不要用。這樣比較慢
        {
            std::shared_ptr<Entity> e2 = std::make_shared<Entity>();
            std::weak_ptr<Entity> e3 = e1;
            e1 = e2;
        }// 內層
    }// 外層
}
  • 如果用 make_shared,reference counting 所需要的記憶體和 Entity instance 是一起配置的所以比較快

  • 出內層的時候雖然 e2 死了,但物件還在,到離開外層的時候因為連 e1 都死了,物件才被消滅

weak_ptr

  • 和 shared_ptr 一起用。上面那段程式裡的 e3 = e1 不會增加 reference count

  • 不想 take ownership of entity 的時候用

  • 可以透過 weak_ptr 問一個物件是否還活著,但 weak_ptr 不會 keep it alive

45. Copying and Copy Constructors in C++

46. The Arrow Operator in C++

47. Dynamic Arrays in C++ (std::vector)

48. Optimizing the usage of std::vector in C++

52. How to Deal with Multiple Return Values in C++

53. Templates in C++

  • 叫 compiler 幫你寫 code(template will be compiled by your usage)

  • 下面這個例子如果要改 Print 的定義就要改兩次

[ ]:
void Print(int value)
{
    std::cout << value << std::end;
}

void Print(float value)
{
    std::cout << value << std::end;
}

int main()
{
    Print(5);
    Print(5.5f);
}
  • template 版定義:

[ ]:
template<typename T>
void Print(T value)
{
    std::cout << value << std::end;
}

int main()
{
    Print<int>(5);
    Print(5.5f);
}
  • 這個 Print 不是真正意義上的函數,而是一個 template。只有被呼叫到的時候才編譯,所以如果沒有被呼叫過,template 裡面就算有文法錯這份 code 還是會編譯成功

  • 呼叫時可以 Print<int>(5); 也可以直接 Print(5);,編譯器會自己 infer

  • std::array template 的定義是像這樣:

[3]:
#include <iostream>

template<typename T, int N>
class Array
{
private:
    T m_Array[N];
public:
    int GetSize() const { return N; }
};

int main()
{
    Array<int, 5> a;
    std::cout << a.GetSize() << std::endl;
}
  • 用 template 是在「program the compiler」,可以改到很複雜。有些公司直接禁用

  • The Cherno 認為不該全面禁用因為 template 還是很強大的。一個很好的應用是 logging system,因為可能想 log 的 type 有無限多種

  • 但如果改到太複雜會很難 debug,只能紙筆寫下追蹤到底編譯了什麼 code

54. Stack vs Heap Memory in C++

  • C++ 程式開始執行時系統配置給該程式的記憶體會被分成五大區塊

    1. Code Segment

      • text, code, functions

    2. Uninitialized Data Segment

    3. Initialized Data Segment

      • static and global variables

    4. Stack Segment

    5. Heap Segment

  • Stack 跟 Heap 都是在記憶體裡,不會預設放在 CPU cache

  • Stack Based Memory:

    • new 出來的記憶體空間就是 Heap based,其它都是 stack based

    • 空間大小編譯時期就必需知道,這樣才能知道程式的 stack 區塊要預留多大

    • Stack based 記憶體只有在該變數的 scope 內才活著

    • Stack memory allocation 只是改 stack pointer(釋放記憶體也一樣),在 assembly 裡就是一個 mov,只有一個 CPU instruction,非常快!

    • Stack 裡所有變數都是擠在一起的,除了中間可能會有一些 safety guard

    • 大部份 stack implementation 都是 grow the stack backward 所以 VS 裡看到的記憶體內容是倒過來的

  • Heap Based Memory:

    • new 出來之後會一直活到被 delete

    • new 其實是呼叫 malloc,在執行期間去找目前能用且夠大的記憶體空間。系統預設會給一個程式一些 free list 空間,如果那些也不夠,程式就要另外向系統要多的空間,系統負責去找出夠大的空間配置給程式並聲明這塊空間被佔用了。很多 bookkeeping 要做,比 stack allocation(1 CPU instruction)慢很多

  • 當情況允許時應該盡量用 stack allocation

  • 這裡比較了兩種 allocation 的 assembly

55. Macros in C++

56. The “auto” keyword in C++

  • The Cherno 自己只有 type 很長的時候時候才會用 auto,例如

    • std::vector<std::string>::iterator

    • std::unordered_map<std::string, std::vector<Device*>>

  • 但其實 type 很長時也可以用 typedefusing

[ ]:
using DeviceMap = std::unordered_map<std::string, std::vector<Device*>>;
[ ]:
typedef std::unordered_map<std::string, std::vector<Device*>> DeviceMap;
  • 如果要用 auto 接 reference,還是要加 & 不然 auto 就會複製一份

  • const 也一樣,要寫 const auto&,不能省略 const&

  • 如果有 template,有些情況下會必需要用 auto,因為不知道函數會回傳什麼類的物件。遇到這種 code 就太複雜了,應該避免

57. Static Arrays in C++ (std::array)

58. Function Pointers in C++

  • 下面的 &PrintValue 指出該函數在記憶體區塊中的位址。& 可以省略,有 implicit conversion

[4]:
#include <iostream>
void PrintValue(int a){
    std::cout << "Values: " << a << std::endl;
}
{
    auto function = &PrintValue;
    function(5);
}
Values: 5
[2]:
{
    auto function = PrintValue;
    function(5);
}
Values: 5
  • 如果不用 auto

[3]:
{
    void(*cherno)(int) = PrintValue;
    cherno(5);
}
Values: 5
  • 太複雜了所以用 typedef

[4]:
{
    typedef void(*PrintValueFunction)(int);

    PrintValueFunction cherno = PrintValue;
    cherno(5);
}
Values: 5
  • ForEach example

[2]:
#include <vector>

void ForEach(const std::vector<int>& values, void(*func)(int))
{
    for(int value : values)
        func(value);
}
{
    std::vector<int> values = {1, 5, 4, 2, 3};
    ForEach(values, PrintValue);
}
Values: 1
Values: 5
Values: 4
Values: 2
Values: 3

59. Lambdas in C++

  • (延續上面的 ForEach example)傳 lambda 給 ForEach

[8]:
#include <vector>
#include <iostream>

void ForEach(const std::vector<int>& values, void(*func)(int))
{
    for(int value : values)
        func(value);
}
{
    int b = 17;

    std::vector<int> values = {1, 5, 4, 2, 3};
    ForEach(values, [](int a){ std::cout << a << ", "; });

    std::cout << std::endl;

    auto lambda = [](int a){ std::cout << a << ", "; };
    ForEach(values, lambda);
}
1, 5, 4, 2, 3,
1, 5, 4, 2, 3,
  • [] 是 capture,用來抓 lambda 外面的變數進來,例如上面例子裡的 b

    • []:no capture

    • [&]:capture all by-reference

    • [&, i]:all by-reference, except i is captured by copy

    • [=]:capture all by-copy

    • [=, &i]:all by-copy capture, except i is captured by reference

    • [a, &b]:a is by-copy and b is by reference

    • cppreference 看完整的 list

  • 就算用 by-value capture 還是沒辦法在 lambda 裡 assign 值給外面的變數,例如上面的例子在 lambda 裡加 b = 5 是不行的。除非把 lambda 宣告成 mutable

  • std::find_if 找 vector 裡第一個大於 3 的數:

[2]:
#include <vector>
#include <iostream>
#include <functional>
{
    std::vector<int> values = {1, 5, 4, 2, 3};
    auto it = std::find_if(values.begin(), values.end(), [](int value){ return value > 3; });
    std::cout << *it << std::endl;
}
5

60. Why I don’t “using namespace std”

  • std:: 可以增加可讀性,如果 project 用了像 EASTL 這種類 STL library,看到 std::vector 一下就知道是在用 STL 版而不是 EASTL 版

  • 如果同一個 scope 中同時有 using namespace std;using namespace eastl;,直接宣告 vector<int> 時會編譯不過(ambiguous)

  • 更糟的狀況是兩個 library 有 signature 類似但不完全相同的函數,如果不指定呼叫的是哪一個函數,有可能因為 implicit conversion 而呼叫錯誤的版本(silent runtime error)

    • 下面這個例子會呼叫到 orange::print

[1]:
#include <iostream>
#include <string>

namespace apple {
    void print(const std::string& text){
        std::cout << text << std::endl;
    }
}
namespace orange {
    void print(const char* text){
        std::string tmp = text;
        std::reverse(tmp.begin(), tmp.end());
        std::cout << tmp << std::endl;
    }
}
[2]:
using namespace apple;
using namespace orange;

print("Hello");
olleH
  • 如果要用 using namespace,在越小的 scope 用越好,函數裡,if statement 區塊

  • The Cherno 在用自己寫的小型 library 時才會用 using namespace。對 STL 這種從來不用

  • 永遠不要在標頭檔裡 using namespace!!!會被 preprocessor 貼的到處都是很難 debug

  • 有四種變數名稱 convention

    • camelCase

    • PascalCase

    • snake_case

    • kebab-case

61. Namespaces in C++

62. Threads in C++

63. Timing in C++

64. Multidimensional Arrays in C++ (2D arrays)

65. Sorting in C++

66. Type Punning in C++

67. Unions in C++

68. Virtual Destructors in C++

  • 下面這個例子如果 ~Base() 前面沒有寫 virtual,poly 被 delete 的時候就不會呼叫 ~Derive() 而出現 memory leak

  • 不同於一般 virtual function,base class virtual destructor 不會被 overwritten,它只是一個機制去讓 C++ 呼叫 derived class destructor

  • 所有會被 derived 的 class 都強烈建議把 destructor 宣告成 virtual

[1]:
#include <iostream>

class Base
{
public:
    Base() { std::cout << "Base Constructor" << std::endl; }
    virtual ~Base() { std::cout << "Base Destructor" << std::endl; }
};

class Derived: public Base
{
public:
    Derived() {
        m_Array = new int[5];
        std::cout << "Derived Constructor" << std::endl;
    }
    ~Derived() {
        delete[] m_Array;
        std::cout << "Derived Destructor" << std::endl;
    }
private:
    int* m_Array;
};

{
    Base* base = new Base();
    delete base;
    std::cout << "-----------------------------------" << std::endl;
    Derived* derived = new Derived();
    delete derived;
    std::cout << "-----------------------------------" << std::endl;
    Base* poly = new Derived();
    delete poly;
}
Base Constructor
Base Destructor
-----------------------------------
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor
-----------------------------------
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor

69. Casting in C++

70. Conditional and Action Breakpoints in C++

71. Safety in modern C++ and how to teach it

72. Precompiled Headers in C++

73. Dynamic Casting in C++

74. BENCHMARKING in C++ (how to measure performance)

75. STRUCTURED BINDINGS in C++

76. How to Deal with OPTIONAL Data in C++

77. Multiple TYPES of Data in a SINGLE VARIABLE in C++?

78. How to store ANY data in C++

79. How to make C++ run FASTER (with std::async)

80. How to make your STRINGS FASTER in C++!

81. VISUAL BENCHMARKING in C++ (how to measure performance visually)

82. SINGLETONS in C++

83. Small String Optimization in C++

84. Track MEMORY ALLOCATIONS the Easy Way in C++

85. lvalues and rvalues in C++

  • 在記憶體裡有位址的就是 lvalue,沒有的(temporary object)就是 rvalue,例如 int i = 10; 裡 i 是 lvalue,10 是 rvalue

  • 函數如果回傳暫存物件也是 rvalue,例如 int i = GetValue();,這裡 GetValue 是一個回傳整數 10 的函數

  • 不能 assign 給 rvalue,例如 10 = i; 因為 temporary object 沒有記憶體位址沒辦法存 assign 進來的東西

    • (所以左邊就一定是 lvalue?)

    • 不一定在右邊就是 rvalue,例如可以 a = i;

    • 如果 GetValue() 回傳 rvalue (如 int),GetValue() = 5; 編譯器會報錯 expression must be a modifiable (non-const) lvalue

  • 要讓 GetValue() = 5; 合法需要回傳 lvalue ref

[ ]:
int& GetValue()
{
    static int value = 10;
    return value;
}
  • lvalue ref 不能接收 rvalue!!!lvalue ref 就是 lvalue 的別名,只能接收 lvalue

[ ]:
void SetValue(int& value){}

int i = 10;
int j = 5;
SetValue(i);    // 合法
SetValue(10);   // 不合法
SetValue(i+j);  // 不合法
  • 但是!!有一個例外:const lvalue ref 可以接收 rvalue

[ ]:
const int& a = 10;  // 合法
int& b = 10;        // 不合法
  • expression 像 a + b 是 rvalue,所以上面的 SetValue(i+j); 不合法,但是用上面的例外把定義改成 void SetValue(const int& value){} 就合法了

    • 所以 C++ 才會那麼多函數的輸入型別都是 const lvalue ref

    • 這樣寫的好處是函數可以接收 lvalue 也可以接受 rvalue

  • C++ 11 開始有只能接收 rvalue 的 rvalue ref int&& value(相對於只能接收 lvalue 的 (non-const) lvalue ref):

[ ]:
void SetValue(int&& value){}

int main()
{
    int i = 10;
    int j = 5;
    SetValue(i);    // 不合法
    SetValue(10);   // 合法
    SetValue(i+j);  // 合法
}
  • 如果 overload(像下面這樣定義兩次),雖然 const lvalue ref 可以接收 rvalue,但傳入 rvalue 時還是會呼叫 rvalue ref 的版本

[ ]:
void SetValue(const int& value){}
void SetValue(int&& value){}

86. Continuous Integration in C++

87. Static Analysis in C++

88. Argument Evaluation Order in C++

89. Move Semantics in C++

  • 有兩種情況需要複製一個物件:

    • return 一個物件時需要的 temp object:有 return value optimization 可以處理

    • 把物件傳進函數裡,想要 take ownership 但不想要實際上 copy 時,其實應該用 move 才對

  • 下面這段程式因為 String 沒有 move ctor,執行會印出 Created! Copied! Cherno,String 的 ctor 和 copy ctor 加起來總共 new 了兩次記憶體,一次在 String("Cherno") 被構造出來的時候,一次是在它被傳進 e1 裡給 m_Name 的時候 Entity 的 ctor 做了一次 copy。如果有 move ctor,Entity e1("Cherno"); 就可以真正傳 rvalue 進到 Entity 裡,那個 copy 就可以省下來

[ ]:
#include<iostream>

class String
{
public:
    String() = default;
    String(const char* string)  // ctor
    {
        printf("Created!\n");
        m_Size = strlen(string);
        m_Data = new char[m_Size];
        memcpy(m_Data, string, m_Size);
    }
    String(const String& other)  // copy ctor
    {
        printf("Copied!\n");
        m_Size = other.m_Size;
        m_Data = new char[m_Size];
        memcpy(m_Data, other.m_Data, m_Size);
    }
    ~String()
    {
        printf("Destroyed!\n")
        delete m_Data;
    }
    void Print()
    {
        for(uint32_t i= 1; i < m_Size; i++)
            printf("%c", m_Data[i]);
        printf("\n");
    }
private:
    char* m_Data;
    uint32_t m_Size;
};

class Entity
{
public:
    Entity(const String& name): m_Name(name){}
    void PrintName()
    {
        m_Name.Print();
    }
private:
    String m_Name;
};

int main()
{
    Entity e1("Cherno");  // 沒有 move,同 Entity e1(String("Cherno"));
    e1.PrintName();
}
  • 為了省下那次 copy,需要寫

    1. 可接收 rvalue 的 Entity ctor

    2. String 的 move ctor。只有 rewire 指標而已,沒有 new

  • Entity ctor:下面這兩個做一樣的事情:把 input cast 成 rvalue。少了這個 cast 還是會呼叫到 String 的 ctor(實務上都是寫 std::move 而不會直接 cast)

[ ]:
Entity(String&& name): m_Name((String&&)name){}
Entity(String&& name): m_Name(std::move(name)){}
  • String 的 move ctor

[ ]:
String(String&& other) noexcept  // move ctor
{
    printf("Moved!\n");
    m_Size = other.m_Size;
    m_Data = other.m_Data;
    other.m_Size = 0;
    other.m_Data = nullptr;
}
  • 為什麼需要 other.m_Data = nullptr;?因為傳進 Entity 裡的 rvalue String 在毀滅的時候一樣會呼叫 dtor,如果沒有在 move ctor 裡把 other 設成 hollow object,當 move 完成的時候 other 和 m_Data 會指著同一塊記憶體,而這塊記憶體馬上就被 rvalue String(在這個例子裡是傳進 Entity ctor 裡的 String&& name)呼叫的 dtor delete 掉了

  • 改完之後執行會印出 Created! Moved! Destoried! Cherno

90. std::move and the Move Assignment Operator in C++

  • 用上面的例子,這兩行 code 會呼叫 copy ctor:

[ ]:
String string = "Hello";
String dest = string;
  • 如果想讓它呼叫 move ctor 而不是 copy,可以用下面任意一種:

[ ]:
String dest = (String&&)string;
String dest((String&&)string);
String dest = std::move(string);
String dest(std::move(string));
  • std::move 做的事就是把傳進來的物件 cast 成 rvalue。如果要 move 一個已經是 rvalue 的物件,就不需要 std::move。如果要 move 一個新構造的物件,也可以不用 std::move 因為 ctor 本來也就會耗資源。但如果是一個已經存在的 lvalue 物件,用 std::move 可以省下 copy 造成的額外負擔

  • std::move 比直接 cast 好因為

  • 以上所有 dest 都是新宣告的,所以呼叫 move ctor。如果 dest 已經存在,就需要 move assignment:

[ ]:
String dest1 = std::move(string);  // 呼叫 move ctor
String dest2;
dest2 = std::move(string);         // 呼叫 move assignment
  • move assignment:

[ ]:
String& operator=(String&& other) noexcept
{
    printf("Moved!\n");
    if (this != &other)
    {
        delete[] m_Data;
        m_Size = other.m_Size;
        m_Data = other.m_Data;
        other.m_Size = 0;
        other.m_Data = nullptr;
    }
    return *this;
}
  • 和 move ctor 有三處不同:

    • 以 ref 的型式回傳自己(*this)

    • 需要 delete[] m_Data; 因為 move assign 之後原本的 data 就不需要了,新的 m_Data 指標被指向新的 data,如果不先 delete 舊的 data 會有 memory leaking

    • 檢查 this != &other 是為了防止有人呼叫 dest = std::move(dest);(把自己傳給自己)

  • 下面這段 code 在 move 之前 dest 是空的,move 完之後變成 apple 是空的:

[ ]:
String apple = "Apple";
String dest;
dest = std::move(apple);
  • 所以現在 C++ 物件必寫函數從四個變成六個:

    • ctor

    • dtor

    • copy ctor

    • copy assignment

    • move ctor

    • move assignment

  • Rule of three/five/zero

    • 3: 如果需要下列三者之一,幾乎可以肯定三者同時需要:custom dtor,copy ctor,copy assignment

    • 5: 需要 move semantics 的 class 要另外寫 move ctor 和 move assignment

    • 0: 如果一個 class 沒有 ownership 的概念,應該上面五個都不要寫(單一職掌原則)

91. ARRAY - Making DATA STRUCTURES in C++

92. VECTOR/DYNAMIC ARRAY - Making DATA STRUCTURES in C++

93. ITERATORS in C++

  • 遍歷 vector 的三種方法:

[1]:
#include <iostream>
#include <vector>

std::vector<int> values = {1, 2, 3, 4, 5};

for(int i=0 ; i<values.size() ; i++)
    std::cout << values[i] << ", ";
1, 2, 3, 4, 5,
[2]:
for(int v : values)
    std::cout << v << ", ";
1, 2, 3, 4, 5,
[5]:
for(std::vector<int>::iterator it=values.begin() ; it!=values.end() ; it++)
    std::cout << *it << ", ";
1, 2, 3, 4, 5,
  • range based for loop 是 iterator 版本的簡寫

  • 必需要有 .begin().end() 才能用 range based for loop 和 iterator

    • values.end() 指的不是最後一個 element 而是 the element after the last

  • 為什麼需要 iterator?

    • 有時候會需要 index 而不只是 element 本身(不然永遠用 range based 就行了),例如想 erase 一個 element 的時候

    • 很多 container 如 tree 和 map 沒有 indexing system

  • 有四種不同的 iterator:

    • const_iterator

    • const_reverse_iterator

    • iterator

    • reverse_iterator

  • iterator 用起來跟 pointer 一樣,因為 implement 了 dereference operator *

  • unordered_map 例子:

    • iterator 指向一個 std::pair

[3]:
#include<iostream>
#include<unordered_map>

std::unordered_map<std::string, int> map;
map["Cherno"] = 5;
map["C++"] = 2;

for(std::unordered_map<std::string, int>::const_iterator it=map.begin();
   it!=map.end() ; it++)
{
    auto& key = it->first;
    auto& value = it->second;

    std::cout << key << " = " << value << std::endl;
}
C++ = 2
Cherno = 5
  • iterator 名字太長了,可以用 using 來簡化

[1]:
#include<iostream>
#include<unordered_map>

using ScoreMap = std::unordered_map<std::string, int>;
ScoreMap map;
map["Cherno"] = 5;
map["C++"] = 2;

for(ScoreMap::const_iterator it=map.begin(); it!=map.end() ; it++)
{
    auto& key = it->first;
    auto& value = it->second;

    std::cout << key << " = " << value << std::endl;
}
C++ = 2
Cherno = 5
  • range based 版本:

[2]:
for(auto kv : map)
{
    auto& key = kv.first;
    auto& value = kv.second;

    std::cout << key << " = " << value << std::endl;
}
C++ = 2
Cherno = 5
  • range based + structured binding(C++17 新功能):

[3]:
for(auto [key, value] : map)
    std::cout << key << " = " << value << std::endl;
C++ = 2
Cherno = 5