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;
}
記憶體零的位置不能讀寫,如果試著去讀程式會 crash,但上面這些 statement 是合法的編譯可以過
-
看到的是反的。
int a = 5;在記憶體裡會看到05 00 00 00
&a:a 的位址,*p:p 裡面住的人cast
int*intodouble*(反正所有 ptr 都是整數)
[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;
}
int* a, b;這樣宣告只有 a 是指標,b 還是整數。如果要兩個指標要宣告成int *a, *b;
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.430extern和(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::astatic 成員變數可以是 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 關鍵字,不是強制的但
可以增加可讀性
可以避免函數名字打錯(如果打成 GETNAME() override 編譯會出錯)
如果父類裡忘了宣告這個函數為 virtual 編譯也會出錯
overhead
父類裡多出 4 bytes 用來存指向子類 vtable 的指標
到了子類要查 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 的差別
lifetime:stack based 只看 scope,heap based 就是 new 出來的,要一直到看到 delete 才會消失
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 放在 * 前後會有差別。總結:只有直接接在 const 後面的東西才是真正不能改
int*的int和*要分開來看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 關鍵字有兩個不同的意義:
使某成員變數在 const 成員函數裡能被改寫,看上面的例子。大部份時候是為了 debug,如果直接把該成員函數改成 non-const 可能會 break 別的東西
lambda(The Cherno 實務上從來沒看過人這麼寫,從來不需要)
auto f = [=]()把 scope 裡所有東西傳進去,by valueauto 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();
}
}
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);,編譯器會自己 inferstd::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++ 程式開始執行時系統配置給該程式的記憶體會被分成五大區塊
Code Segment
text, code, functions
Uninitialized Data Segment
Initialized Data Segment
static and global variables
Stack Segment
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>::iteratorstd::unordered_map<std::string, std::vector<Device*>>
但其實 type 很長時也可以用
typedef或using
[ ]:
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-
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){}
-
例如上面的
SetValue(i+j);不用為i + j配置記憶體
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,需要寫
可接收 rvalue 的 Entity ctor
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 好因為有些情況下 cast 會失敗
增加 code 可讀性
以上所有 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
-
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 和 iteratorvalues.end()指的不是最後一個 element 而是 the element after the last
為什麼需要 iterator?
有時候會需要 index 而不只是 element 本身(不然永遠用 range based 就行了),例如想 erase 一個 element 的時候
很多 container 如 tree 和 map 沒有 indexing system
有四種不同的 iterator:
const_iteratorconst_reverse_iteratoriteratorreverse_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