因為我是開始學 DX 後才跟 C++ 熟起來的,所以今天要來說明從其他語言到 C++ 最有可能遇到的難關指標,與其他底層比較會碰到的部分。
一般物件或著其他的程式語言沒特別設定的話,你拿到的會是 obj 本身,基本上可以操作的範圍只有 data 這個值
Normal object
MyObject obj;
┌───────────────┐
│ obj │
│ │
│ MyObject │
│ data = 10 │
└───────────────┘
而在 C++ 語法裡,當你用 * 標示時,你會拿到的是 MyObject 所在的記憶體位址,並存在 ptr 裡面
Pointer
MyObject* ptr;
┌───────────────┐
│ ptr │
│ 0x00001000 ───────────────┐
└───────────────┘ │
▼
0x00001000
┌───────────────┐
│ MyObject │
│ data = 10 │
└───────────────┘
因為指標可以直接改值、甚至操作記憶體,是個很容易造成 crash 的寫法。也因為生命週期都是自己處理,一不小心就會造成記憶體洩漏的問題。所以現在都不太建議直接操作指標了,而是使用 unique_ptr 跟 shared_ptr 這類 smart pointer,來自己處理物件的生命週期。
unique_ptr 表示「只有一個擁有者」的 smart pointer。當 unique_ptr 離開作用域時,會自己自動釋放記憶體。
std::unique_ptr<int> p = std::make_unique<int>(10); //宣告方式
unique_ptr 無法被複製,如果要轉移所有權,要使用 std::move
std::unique_ptr<int> b = a //會報錯
std::unique_ptr<int> b = std::move(a); //要這樣寫才能成功轉移
class Mesh
{
public:
Mesh() {}
};
// Mesh 只會有一個
std::unique_ptr<Mesh> mesh = std::make_unique<Mesh>();
shared_ptr 表示「多個地方共同擁有同一個物件」。
它的內部會記錄目前有多少個 shared_ptr 正在指向這個物件。當最後一個 shared_ptr 被釋放時,物件才會被刪除。
std::shared_ptr<int> a = std::make_shared<int>(10);
std::shared_prt<int> b = a; // a 跟 b 都指向同一個 int
std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>();
//如果 Renderer 和 CollisionSystem 都需要共同使用同一個 Mesh,而且不確定誰先被釋放,就可以考慮 shared_ptr。
Renderer renderer(mesh);
CollisionSystem collision(mesh);
static_cast 是用來在編譯期決定轉換是否合法與如何轉換
int a = 10;
float b = static_cast<float>(a); // 把 int 轉成 float
dynamic_cast 則用在有繼承關係的類別之間進行安全的型別轉換,會在執行時檢查是否可轉換
class Animal
{
public:
virtual ~Animal() = default;
};
class Dog : public Animal
{
};
Animal* animal = new Dog();
Dog* dog = dynamic_cast<Dog*>(animal); //嘗試確認 animal 實際上是不是 Dog,如果是,就取得 Dog*
reinterpret_cast 是很危險的型別轉換。它會把某段記憶體「重新解釋」成另一種型別。一般比較上層的開發是見不到它的,但因為我們是在操作 DX,自然會用到此功能。
int value = 100;
char* bytes = reinterpret_cast<char*>(&value); //把 int* 當成 char* 來看,用來讀取它的 byte 內容
應該在演算法競賽的時候,偶而會有題目是涉及到位元運算的。實際上在寫 Shader 時,確實偶而會遇到這樣的需求來優化運算的速度。這邊來稍微帶過。
bit 的 and,只有兩邊都為同個值的時候才會是該值
Ex:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
bit 的 or,只要一邊為 1 結果就為 1
Ex:
0 | 0 = 0
1 | 0 = 1
1 | 1 = 1
0 | 1 = 1
<< 為 bit 往左移動
Ex:
0000 //數字為 0
往左移動 1 bit
0000 //數字為 0
0001 //數字為 1
往左移動 1 bit
0010 //數字為 2
0010 //數字為 2
往左移動 1 bit
0100 //數字為 4
>>為 bit 往右移動,後面都跟上面同概念
這裡最簡單的用法之一,是如果今天試圖做屬性標籤,可以用此來決定其包含哪些標籤。
constexpr int TAG_PLAYER = 1 << 0; // 0001
constexpr int TAG_ENEMY = 1 << 1; // 0010
constexpr int TAG_FLYING = 1 << 2; // 0100
constexpr int TAG_BOSS = 1 << 3; // 1000
...
int tags = TAG_ENEMY | TAG_FLYING | TAG_BOSS; //會飛的敵方 BOSS
接下來會蠻常見的寫法,用法也很單純,可以用來提醒使用者這個回傳值是有意義的,不該被隨意忽略。畢竟在底層操作中每個物件的存在跟被使用狀態的追蹤是很重要的,只要一點小差錯就可能導致整個程式在不明的地方停止運作。
[[nodiscard]]
ID3D12Resource* getID3DResourceAddress() const {
return m_vertexBuffer.Get();
}
...
int main() {
getID3DResourceAddress(); //叫了但不用編譯器可能會跳警告
}