C++學習筆記-Cherno C++系列

random_d發表於2024-11-23

21-23.【Cherno C++】C++中的靜態(static)

  • static變數只在編譯單元內部連結
    • 靜態變數的作用域只在單個檔案內
    • 建議:在非特殊情況下,永遠使用static定義全域性變數以限制作用域
  1. 全域性變數重複定義
/* a.cpp */
int g_Variable = 5;
/* main.cpp */
#include <iostream>  
int g_Variable=10;  
int main(){  
    std::cout<<s_Variable<<std::endl;  
}
  1. 使用static限制a.cpp中全域性變數的作用域
/* a.cpp */
static int s_Variable = 5;
/* main.cpp */
#include <iostream>  
int s_Variable=10;  
int main(){  
    std::cout<<s_Variable<<std::endl;  
}
  1. 還可以使用external關鍵詞在main.cpp中定義外部變數
/* a.cpp */
int s_Variable = 5;
/* main.cpp */
#include <iostream>  
extern int s_Variable;  
int main(){  
    std::cout<<s_Variable<<std::endl;  
}
  1. 如果外部變數被標記為static,則main.cpp無法連結外部變數
/* a.cpp */
static int s_Variable = 5;
/* main.cpp */
#include <iostream>  
extern int s_Variable;  
int main(){  
    std::cout<<s_Variable<<std::endl;  
}
  • 類和結構體中的靜態變數和靜態方法
    • 用來描述類或結構體自身的屬性
    • 靜態方法不能訪問非靜態變數
#include <iostream>

struct Entity
{
    int x, y;

    void Print() const {
        std::cout << x << ", " << y << std::endl;
    }
};

struct Entity_Static
{
    static int x, y;

    static void Print() {
        std::cout << x << ", " << y << std::endl;
    }
};

// 對於靜態變數,如果要透過例項訪問需要事先初始化,該初始化語句不能寫在函式體內
int Entity_Static::x;
int Entity_Static::y;

int main() {
    Entity e{};
    e.y = 2;
    e.x = 3;
    Entity e1{5, 6};
    e.Print();  //3,2
    e1.Print(); //5,6

    Entity_Static es1;
    es1.x = 2;
    es1.y = 5;

    Entity_Static es2;
    es2.x = 6;
    es2.y = 8;
    es1.Print(); //6,8
    es2.Print(); //6,8
    /* 對於靜態變數和方法,上述的引用沒有任何意義,可以直接使用類或結構體名稱引用 */
    Entity_Static::x = 1;
    Entity_Static::y = 2;
    Entity_Static::Print();
}
/* 靜態方法不能訪問非靜態變數 */
# include <iostream>  
  
struct Entity  
{  
    int x, y;  
	// Invalid use of member 'x' in static member function
    static void Print() {  
        std::cout << x << "," << y << std::endl;  
    }  
};
/* 靜態方法不能訪問非靜態變數的原因在於:靜態方法找不到非靜態變數的例項 */
/* 透過以下方法可以實現靜態方法訪問非靜態變數 */
#include <iostream>

struct Entity
{
    int x, y;

    static void Print(const Entity& e) {
        std::cout << e.x << ", " << e.y << std::endl;
    }
};
  • 區域性作用域中的static
    • 延長區域性變數的生存週期【其生存週期基本上相當於整個程式的生存期】
#include <iostream>

void FunctionStatic() {
    static int i = 10;
    i++;
    std::cout << i << ", ";
}

void Function() {
    int i = 10;
    i++;
    std::cout << i << ", ";
}

int main() {
    /* 靜態變數i在函式FunctionStatic執行完畢後不會銷燬 */
    FunctionStatic();
    FunctionStatic();
    FunctionStatic();
    FunctionStatic();
    FunctionStatic();
    std::cout << std::endl;
    /* 非靜態變數i在函式Function執行完畢後會銷燬 */
    Function();
    Function();
    Function();
    Function();
    Function();
    std::cout << std::endl;
}
  • 區域性靜態(Local Static)的應用
/* 單例模式 */
/* 老方法:使用類或結構體 */
#include <iostream>  
#include <string>  
  
class Singleton  
{  
private:  
    std::string name = "noName.";  
    static Singleton* s_Instance;  
public:  
    static Singleton& Get() { return *s_Instance; }  
  
    void Hello() { std::cout << "Hello " << name << std::endl; }  
};  
  
Singleton* Singleton::s_Instance = nullptr;  
  
int main() {  
    Singleton::Get().Hello();  
    return 0;  
}
/* 新方法:使用區域性靜態(Local Static) */
#include <iostream>
#include <string>

class Singleton
{
private:
    std::string name = "noName.";
public:
    static Singleton& Get() {
        static Singleton instance;
        return instance;
    }

    void Hello() { std::cout << "Hello " << name << std::endl; }
};

int main() {
    Singleton::Get().Hello();
    return 0;
}

44.【Cherno C++】C++的智慧指標

需要包含標頭檔案memory

  • std::unique_ptr
    • 不能複製unique_ptr,指向同一區域的unique_ptr只能有一個
    • 使用std::make_unique<Entity>()對unique_ptr進行安全的初始化
#include <iostream>
#include <memory>

class Entity
{
public:
    Entity() {
        std::cout << "Created Entity!" << std::endl;
    }

    ~Entity() {
        std::cout << "Destroyed Entity!" << std::endl;
    }

    void Print() {
        std::cout << "Print Successful." << std::endl;
    }
};

int main() {
    // std::unique_ptr
    /* 只能顯式呼叫 */
    /* 不能使用 std::unique_ptr<Entity> entity = new Entity();
     * 這種隱式呼叫方式 */
    std::unique_ptr<Entity> entity(new Entity());
    entity->Print();

    // 更加安全的呼叫方式
    /* 能夠處理建構函式的呼叫異常 */
    std::unique_ptr<Entity> entity1 = std::make_unique<Entity>();
    entity1->Print();
    // unique_prt不能被複制,不能寫成如下形式。
    // std::unique_ptr<Entity> entity0 = entity1;
}
  • std::shared_ptr
    • 可以複製,透過引用計數追蹤shared_ptr被引用了多少次
    • 使用std::make_shared<Entity>()對shared_ptr進行安全的初始化
#include <iostream>  
#include <memory>  
  
class Entity  
{  
public:  
    Entity() {  
        std::cout << "Created Entity!" << std::endl;  
    }  
  
    ~Entity() {  
        std::cout << "Destroyed Entity!" << std::endl;  
    }  
  
    void Print() {  
        std::cout << "Hello" << std::endl;  
    }  
};  
  
int main() {  
    // std::shared_ptr  
 std::shared_ptr<Entity> entity;  
    {  
        auto sharedEntity = std::make_shared<Entity>();  
        sharedEntity->Print();  
        // shared_ptr 可以複製  
 entity = sharedEntity;  
    }  
    // 由於sharedEntity在函式作用域外仍然有引用,  
 // 所以sharedEntity在程式執行結束後才釋放  
 entity->Print();  
}
  • std::weak_ptr
    • 通常和shared_ptr連用
      • shared_ptr賦值給shared_ptr會增加shared_ptr的引用次數
      • shared_ptr賦值給weak_ptr不會增加引用次數
    • 注意:weak_ptr不會檢測自己是否還有效
#include <iostream>
#include <memory>

class Entity
{
public:
    Entity() {
        std::cout << "Created Entity!" << std::endl;
    }

    ~Entity() {
        std::cout << "Destroyed Entity!" << std::endl;
    }

    void Print() {
        std::cout << "Hello" << std::endl;
    }
};

int main() {
    // std::weak_ptr
    std::weak_ptr<Entity> entity;
    {
        auto sharedEntity = std::make_shared<Entity>();
        sharedEntity->Print();
        // shared_ptr 可以複製
        entity = sharedEntity;
    }
    // weak_ptr指向的shared_ptr在作用域外已經釋放,
    // 此處的weak_ptr變數entity指向了無效記憶體區域
    //entity->Print();
}

為什麼能夠返回unique_ptr?

編譯器對return的解析如下:

  1. 如果能夠move,那麼就呼叫move構造(移動建構函式)。
  2. 如果無法move,那就呼叫copy構造(複製建構函式)。
  3. 如果無法copy,那就報錯(不能move也不能copy,說明該物件無法從函式體內部返回)。

顯然:

  1. unique_ptr<T>具有移動建構函式,可以從函式體內部返回。
  2. 由於是移動構造,當前函式體外的_nodes和函式體內的_nodes是同一個物件。

45.【Cherno C++】C++複製與複製建構函式

  • 淺複製 與 深複製
#include <iostream>
#include <cstring>

struct test2Struct
{
    int x{};
};

class StringManually
{
private:
    char* m_Buffer;
    unsigned int m_Size;
public:
    // 去除explicit,允許進行隱式轉換
    StringManually(const char* string)
            : m_Size(strlen(string)) {
        // m_Size+1 字串要以 \0 符號結尾以終結輸出
        m_Buffer = new char[m_Size + 1];
        memcpy(m_Buffer, string, m_Size);
        // 手動新增終止符
        m_Buffer[m_Size] = '\0';
    }

    StringManually(const StringManually& other)
            : m_Size(other.m_Size) {
        m_Buffer = new char[m_Size + 1];
        memcpy(m_Buffer, other.m_Buffer, m_Size);
        m_Buffer[m_Size] = '\0';
    }

    ~StringManually() {
        delete[] m_Buffer;
    }

    // 過載運算子[]
    char& operator[](const unsigned char index) const {
        return m_Buffer[index];
    }
};

std::ostream& operator<<(std::ostream& stream, const StringManually& string) {
    stream << string.m_Buffer;
    return stream;
}

// 隱式轉換 char* => StringManually
void test1() {
    StringManually string = "No explicit in StringManually.";
    std::cout << string << std::endl;
}

/* 沒有複製建構函式的情況下,下列複製方式均為淺複製 */
// 淺複製 【a和b指向同一記憶體區域,b.x的值改變導致a.x的值改變】
void test2() {
    auto a = new test2Struct;
    auto b = a;
    a->x = 5;
    b->x = 10;
    std::cout << a->x << std::endl;
}

// 淺複製 【a和b指向同一記憶體區域,導致兩次記憶體釋放】
void test3() {
    StringManually a = "li";
    StringManually b = a;
    // 兩次記憶體釋放
    std::cout << b << std::endl;
}

// 深複製 【複製建構函式】
void test4() {
    StringManually string = "Cherno";
    StringManually second(string);
    second[2] = 'a';
    std::cout << string << std::endl;
    std::cout << second << std::endl;
}

int main() {
    test1();
    test2();
    test4();
    test3();
    return 0;
}

46.【Cherno C++】C++的箭頭運算子與點運算子

  • 箭頭運算子用於獲取指標指向的物件(類、結構體)中的成員
  • 點運算子直接獲取物件(類、結構體)中的成員
#include <iostream>
#include <string>
#include <utility>

class Entity
{
private:
    int m_Id{};
    std::string m_Name{};

public:
    Entity(const int& id, std::string name)
            : m_Id(id), m_Name(std::move(name)) {};

    Entity(const Entity& other) = delete;

    ~Entity() = default;

    void Print() const {
        std::cout << "Hello, " << m_Name
                  << ", your Id is " << m_Id << std::endl;
    }
};

int main() {
    // 建立指標,該指標引用在堆上建立的Entity類例項
    auto* entityHeap = new Entity(200, "li");
    // 使用 -> 運算子訪問
    entityHeap->Print();
    // 解引用後可以使用 . 運算子訪問
    /* 注意優先順序,點運算子優先順序高於解引用 */
    (*entityHeap).Print();
}
  • 使用箭頭運算子獲取記憶體中某個成員變數的偏移量
/* 這就是魔法 */
#include <iostream>

struct Vector3
{
    float x, z, y;
};

int main() {
    /* 1. 將0(nullptr)轉換為一個Vector3的指標,表示地址從0開始偏移
     * 2. 用箭頭運算子訪問Vector3中的成員
     * 3. 用取址運算子&,獲取這個x的地址,此時x的地址就是偏移後的地址
     * 4. 將該地址轉換為long long*/
    auto offset_x = (long long) &((Vector3*) nullptr)->x;
    auto offset_y = (long long) &((Vector3*) nullptr)->y;
    auto offset_z = (long long) &((Vector3*) nullptr)->z;
    std::cout << offset_x << ", " << offset_y << ", " << offset_z << std::endl;
}

47.【Cherno C++】C++的動態陣列

  • std::vector(坑爹的名字)
    • 在C++中的意思是動態陣列(Dynamic Array List)
    • 注意:和數學中的向量沒有任何關係。
#include <iostream>
#include <vector>

struct Vector3
{
    float x, y, z;
};

// 過載運算子<<
std::ostream& operator<<(std::ostream& stream, const Vector3& vector3) {
    stream << vector3.x << ", " << vector3.y << ", " << vector3.z << std::endl;
    return stream;
}

int main() {
    std::vector<Vector3> vertices{};
    // 使用push_back()向vector中新增東西,
    /* 根據類的特性可以使用建構函式,也可以直接使用初始化列表。*/
    vertices.push_back({1, 2, 3});
    vertices.push_back({4, 5, 6});
    vertices.push_back({7, 8, 9});
    // C++17新特性,範圍for迴圈,等價於C#中的foreach(element in Elements)
    for (const auto& element: vertices) {
        std::cout << element << std::endl;
    }
    // 從vector中移除元素
    /* 注意:此處不能直接傳入元素索引,需要用迭代器(iterator)
     * 可以從頭刪除【vertices.begin()】,也可以從末尾刪除【vertices.end()】 */
    vertices.erase(vertices.begin() + 1);
}

48.【Cherno C++】C++的stdvector使用最佳化

#include <iostream>
#include <vector>

struct Vector3
{
    float m_x, m_y, m_z;

    Vector3(float x, float y, float z)
            : m_x(x), m_y(y), m_z(z) {}

    Vector3(const Vector3& other)
            : m_x(other.m_x), m_y(other.m_y), m_z(other.m_z) {
        std::cout << "Copied!" << std::endl;
    }

    ~Vector3() = default;
};

int main() {
    // 6次複製,每次賦值時vector調整了兩次大小
    auto* vertices = new std::vector<Vector3>{};
    vertices->push_back(Vector3(1, 2, 3));
    vertices->push_back(Vector3(4, 5, 6));
    vertices->push_back(Vector3(7, 8, 9));
    delete vertices;
    std::cout << std::endl;

    // 最佳化1:每次賦值vector只調整一次大小
    /* 建立vertices2 */
    auto* vertices2 = new std::vector<Vector3>;
    /* 讓vector的容量一次增加3 */
    vertices2->reserve(3);
    /* 只需要3次複製 */
    vertices2->push_back(Vector3(1, 2, 3));
    vertices2->push_back(Vector3(4, 5, 6));
    vertices2->push_back(Vector3(7, 8, 9));
    delete vertices2;
    std::cout << std::endl;

    // 最佳化2:在最佳化1的基礎上使用emplace_back替代push_back
    /* emplace_back只傳遞用於構造Vector3例項的引數,而不是傳遞已經構建的物件,從而省去了複製操作(不會呼叫複製建構函式) */
    auto* vertices3 = new std::vector<Vector3>;
    vertices3->reserve(3);
    vertices3->emplace_back(1, 2, 3);
    vertices3->emplace_back(4, 5, 6);
    vertices3->emplace_back(7, 8, 9);
    delete vertices3;
    /* 在棧上建立vector例項效果相同 */
    std::vector<Vector3> vertices4;
    vertices4.reserve(3);
    vertices4.emplace_back(1, 2, 3);
    vertices4.emplace_back(4, 5, 6);
    vertices4.emplace_back(7, 8, 9);
}

54.【Cherno C++】C++中的棧和堆

#include <iostream>

int main() {
    // 在棧上分配int
    int value = 10;
    int array[] = {1, 2, 3, 4, 5};
    std::cout << value << std::endl;
    for (auto& element: array) std::cout << element << ", ";
    std::cout << std::endl;
    
    // 在堆上分配int
    auto* hValue = new int{5};
    auto* hArray = new int[]{1, 2, 3, 4, 5};
    std::cout << *hValue << std::endl;
    for (int i = 0; i < 5; i++) std::cout << hArray[i] << ", ";
    delete hValue;
    delete[] hArray;
}
  • 注意:在堆上分配記憶體有額外開銷,儘量在棧上分配記憶體,如果在堆上分配記憶體則儘量進行堆記憶體預分配。

58.【Cherno C++】C++的函式指標(原始函式指標)

  • 使用auto或直接使用函式指標對應的型別名稱定義
#include <iostream>
#include <string>

void Hello() {
    std::cout << "hello." << std::endl;
}

void HelloWithInput(const std::string& name, const int& age) {
    std::cout << "Hello " << name << ", your age is " << age << ". " << std::endl;
}

int main() {
    // 定義了一個指向Hello函式的指標HelloPtr,第二個等式左邊的型別是auto推測出的實際型別
    auto HelloPtrWithAuto = &Hello;
    void (* HelloPtr)() =&Hello;
    // 透過函式指標呼叫Hello函式
    HelloPtrWithAuto();
    HelloPtr();
    // 定義一個帶有形參的函式指標,下面第一個等式左邊的型別是auto推測出的實際型別
    void (* HelloWithInputPtr)(const std::string&, const int&) = &HelloWithInput;
    auto HelloWithInputPtrWithAuto = &HelloWithInput;
    HelloWithInputPtr("li", 25);
    HelloWithInputPtrWithAuto("li", 25);
}
  • 使用typedef宣告函式指標
#include <iostream>  
#include <string>  
  
void Hello(const std::string& name, const int& age) {  
    std::cout << "Hello " << name << ", your age is " << age << ". " << std::endl;  
}  
  
int main() {  
    typedef void(* HelloFunction)(const std::string&, const int&);  
    HelloFunction function = &Hello;  
    function("li",25);  
}
  • 函式指標的實際應用(lambda表示式)
#include <iostream>
#include <vector>

void PrintValue(const int& value) {
    std::cout << "Value: " << value << std::endl;
}

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

int main() {
    std::vector<int> values = {1, 5, 4, 2, 3};
    ForEach(values, PrintValue);
}
#include <iostream>
#include <vector>
// 在形參中宣告瞭一個函式指標
void ForEach(const std::vector<int>& values, void(* func)(const int&)) {
    for (const int& value: values) {
        func(value);
    }
}

int main() {
    std::vector<int> values = {1, 5, 4, 2, 3};
    // lambda表示式
    ForEach(values, [](const int& value) {
        std::cout << "Value: " << value << std::endl;
    });
}

59.【Cherno C++】C++的lambda表示式

![[Cpp中lambda語句的組成部分.png]]

  1. 捕獲子句(C++規範中稱為 引導)
    • [=] 透過值捕獲lambda作用域內的所有變數
    • [&] 透過引用捕獲lambda作用域內所有的變數
    • [&a]透過引用捕獲lambda作用域內的變數a
    • [a] 透過值捕獲lambda作用域內的變數a
    • [ ] 表示不捕獲lambda作用域內的任何變數
  2. 引數列表(也稱為lambda宣告符)【非必須】
    • 在C++14標準之後,可以使用auto關鍵字作為型別說明符
    • 如果不需要引數列表,即不需要向lambda表示式傳入引數,則可省略空括號
  3. 可變規範(mutable關鍵詞)【非必須】
    • 通常,lambda 的函式呼叫運算子是按值常數量(const by value),使用mutable關鍵字會取消按值傳遞的外部變數在lambda表示式主體中的const屬性,從而使lambda表示式的主體可以修改透過值捕獲的變數。
    • 總結:捕獲子句按值捕獲時,傳入lambda表示式的副本只讀不可修改,需要mutable修飾以更改外部變數在lambda表示式中的副本
  4. 異常規範(throw())【非必須】
    • 現代C++建議使用noexcept關鍵詞替代throw
    • 與普通函式一樣,如果lambda表示式使用noexcept宣告瞭異常規範並且lambda主體引發異常,編譯器將生成警告。
  5. 尾隨返回型別【非必須】
    • 用於指定lambda表示式的返回型別,可以使用auto關鍵詞自動推導返回型別,如果返回型別為確定為void則不要使用auto
  6. lambda表示式主體
    • 在C++14標準之後,可以在lambda表示式主體中引入新變數

注:可以將lambda表示式宣告為constexpr,或在常量表示式中使用它,在常量表示式中允許對每個捕獲或引入的資料成員進行初始化。

#include <algorithm>
#include <iostream>
#include <vector>
#include <string>

using namespace std;

template<typename T>
void Print(const string& string, const T& data) {
    cout << string;

    for (const auto& element: data) {
        cout << element << " ";
    }

    cout << endl;
}

void fillVector(vector<int>& v) {
    // nextValue為靜態全域性變數,作用域在其所在的.cpp檔案內,函式fillVector執行完畢後nextValue不會被銷燬
    static int nextValue = 1;
    // 為容器的各個元素賦值,其第三個引數gen需要為lambda表示式或函式
    // 注意:該方法不是執行緒安全的,應儘量避免使用,這裡只用作演示
    generate(v.begin(), v.end(), [] { return nextValue++; });
}

int main() {
    const int elementCount = 9;
    // 初始化一個vector<int>變數,初始值有9個1
    vector<int> vertices(elementCount, 1);
    // 定義兩個變數x,y初始化為1
    int x{1}, y{1};
    // 計算兩個元素的和,並將結果按順序存放在vertices中
    generate_n(vertices.begin() + 2,
               elementCount - 2,
               [=]() mutable noexcept -> int { //x,y按照const int形式複製進lambda域中,使用mutable讓lambda域中的x,y可被修改
                   int n = x + y;
                   // 使用mutable修飾後,現在lambda域中的x,y可被修改
                   x = y;
                   y = n;
                   return n;
               });
    Print("vector vertices after call to generate_n() with lambda: ", vertices);

    // lambda表示式,只改變在lambda副本內的x,y的值,不會改變作用域之外的x,y的值。
    cout << "x: " << x << " y: " << y << endl;

    // Fill the vector with a sequence of numbers
    fillVector(vertices);
    Print("vector vertices after 1st call to fillVector(): ", vertices);
    // Fill the vector with the next sequence of numbers
    fillVector(vertices);
    Print("vector vertices after 2nd call to fillVector(): ", vertices);
}
#include <iostream>
#include <vector>
#include <functional>

// 原始指標
void ForEachRaw(const std::vector<int>& values, void(* func)(const int&)) {
    for (const int& value: values) {
        func(value);
    }
}

// 使用std::function
void ForEach(const std::vector<int>& values, const std::function<void(int)>& func) {
    for (const int& value: values) {
        func(value);
    }
}

int main() {
    std::vector<int> values = {1, 5, 4, 2, 3};
    // lambda表示式
    ForEachRaw(values, [](const int& value) {
        std::cout << "Value: " << value << std::endl;
    });
    int a = 5;
    // [=]透過值傳遞所有變數,[&]透過引用傳遞所有變數,
    // [&a]透過引用傳遞變數a,[a]透過值傳遞變數a
    auto lambda = [&](const int& value) {
        std::cout << "Value: " << a << std::endl;
    }; // values有5個元素,func(value)執行了5次,每次都輸出a=5,即a被輸出了5次
    ForEach(values, lambda);
}
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

// foreach接收一個vector<int>型別的引數和一個返回值為void,引數型別為const int&的函式
void ForEach(const std::vector<int>& values, const std::function<void(const int&)>& func) {
    for (const int& value: values) func(value);
}

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

    auto lambda = [](const int& value) {
        std::cout << value << " ";
    };
    ForEach(values, lambda);
    std::cout << std::endl;
    auto lambda1 = [](const int& value) {
        return value > 3;
    };
    // 在陣列中搜尋第一個滿足條件的值並輸出
    auto it = std::find_if(values.begin(), values.end(), lambda1);
    std::cout << *it << std::endl;

    // lambda特性完全使用
    int a{0};
    std::string name{"li"};
    auto lambda2 = [a, name](const int& value)mutable -> std::tuple<int&, std::string&> {
        a += value;
        name = name.append(" Sir. ");
        return {a, name};
    };
    auto[newA, newName]=lambda2(10);
    std::cout << newA << ", " << newName << std::endl;
    newA=100;
    newName="liu";
    std::cout << newA << ", " << newName << std::endl;
}

62.【Cherno C++】C++的執行緒

#include <iostream>
#include <thread>

static bool sg_Finished = false;
static int sg_times = 0;

void GetThreadId() {
    std::cout << "Started thread Id: " << std::this_thread::get_id() << std::endl;
}

void DoWork(const int& sleepTime_secends) {
    GetThreadId();
    while (!sg_Finished) {
        std::cout << "Working..." << std::endl;
        sg_times++;
        std::this_thread::sleep_for(std::chrono::seconds(sleepTime_secends));
    }
}

void output(const int& times) {
    std::cout << "Finished. Print: " << times << " times." << std::endl;
}

int main() {
    // thread型別接收的是一個函式指標
    std::thread worker(DoWork,1);
    std::cin.get();
    sg_Finished = true;
    // 等待該執行緒工作完成
    /* 該功能在C#等現代程式語言中叫做wait或wait for exit */
    worker.join();
    output(sg_times);
    GetThreadId();
}

63.【Cherno C++】C++的計時

#include <iostream>
#include <chrono>
#include <thread>

void TimeCount() {
    // 記錄當前時間
    auto start = std::chrono::high_resolution_clock::now();
    // 更加友好的定時時間設定
    using namespace std::literals::chrono_literals;
    std::this_thread::sleep_for(1s);
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<float> duration = end - start;
    std::cout << duration.count() << std::endl;
}

int main() {
    TimeCount();
}
  • 定義一個Timer類,實現在其作用域內自動計時
#include <iostream>
#include <chrono>
#include <thread>

struct Timer
{
    std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<long long, std::nano>> m_start, m_end;
    std::chrono::duration<float> m_duration{};

    Timer() {
        m_start = std::chrono::high_resolution_clock::now();
    }

    ~Timer() {
        m_end = std::chrono::high_resolution_clock::now();
        m_duration = m_end - m_start;
        std::cout << "Timer took " << m_duration.count() << " seconds." << std::endl;
    }
};

int main() {
    Timer timer;
    // 更加友好的定時時間設定
    using namespace std::literals::chrono_literals;
    std::this_thread::sleep_for(1s);
}

65.【Cherno C++】C++標準庫std::sort

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> values1 = {3, 5, 1, 4, 2};
    std::vector<int> values2(values1);
    std::sort(values1.begin(), values1.end(), std::greater<int>());
    for (auto value: values1) std::cout << value << " ";
    std::cout << std::endl;
    auto lambda = [](int a, int b) -> bool { return a < b; };
    std::sort(values2.begin(), values2.end(), lambda);
    for (auto value: values2) std::cout << value << " ";
    std::cout << std::endl;
}

66.【Cherno C++】C++型別雙關

  • 對於Java或C#,它們的型別系統很難繞開,但在C++中可以透過指標直接訪問記憶體的方式繞開型別系統。【僅作演示用,如非必要不應該繞開型別系統】
#include <iostream>

int main() {
    int a = 50;
    double value = a;
    std::cout << value << std::endl;
    // 型別雙關,將int的值當作double處理
    /* 僅作演示用,可能佔用了其他程式的4個位元組 */
    value = *(double*) &a;
    std::cout << value << std::endl;
}
#include <iostream>

struct Entity
{
    int m_x{}, m_y{};
};

int main() {
    Entity e = {5, 8};
    // 將Entity當作陣列處理
    int* position = (int*) &e;
    std::cout << position[0] << ", " << position[1] << std::endl;
    // 手動間接定址*(-_-)*
    int* positionOf_y = (int*) ((char*) &e + 4);
    std::cout << *positionOf_y << std::endl;
}

73.【Cherno C++】C++的dynamic_cast

  • 類似於CSharp中的關鍵字as
    • 如果可以進行型別轉換則執行型別轉換並返回
    • 如果不能進行轉換則返回null
  1. CSharp程式碼如下:
namespace ConsoleApp1;
class Program
{
    public static void Main(string[] args)
    {
        var myclass2= new MyClass2(2019);
        var myclass1=new MyClass1(24,"li");
        MyClass2? reverseMyClass1ToMyClass2 = myclass1 as MyClass2;
        CheckReverse(reverseMyClass1ToMyClass2);
        var reverseMyClass2ToMyClass1 = myclass2 as MyClass1;
        CheckReverse(reverseMyClass2ToMyClass1);
    }
    private static void CheckReverse(object? reverseMyClass1ToMyClass2)
    {
        if (reverseMyClass1ToMyClass2 == null) { Console.WriteLine("reversedVar is null."); }
        else { Console.WriteLine("reversedVar is Not null."); }
    }
}
class MyClass1
{
    internal int? Age { get; set; }
    internal string? Name { get; set; }
    public MyClass1()
    {
        Age = null;
        Name = null;
    }
    public MyClass1(int age,string name)
    {
        Age = age;
        Name = name;
    }
}
class MyClass2 : MyClass1
{
    internal int? Id { get; set; }
    public MyClass2()
    {
        Id = null;
    }
    public MyClass2(int id){ Id = id; }
}
  1. Cpp程式碼如下:
  • 使用dynamic_cast的前提是編譯器儲存了“執行時型別資訊(Run-Time Type Information)”,類似於C#等託管型語言的“執行時(Runtime)”
#include <iostream>  
  
class Entity  
{  
public:  
    virtual void PrintName() {}  
};  
  
class Player : public Entity  
{  
};  
  
class Enemy : public Entity  
{  
};  
  
int main() {  
    Player* player = new Player();  
    // 隱式轉換  
 Entity* actuallyPlayer = player;  
    Entity* actuallyEnemy = new Enemy();  
  
    // 隱式轉換後再轉換回來會出問題,認不得e是Player還是Enemy  
 //Player* p = actuallyPlayer;  
 // 解決辦法1:顯式型別轉換,依靠人為判斷,容易引來風險  
 Player* p = (Player*) actuallyPlayer;  
    /* 可以人為將actuallyPlayer轉換為Enemy */  
 Enemy* e1 = (Enemy*) actuallyPlayer;  
  
    // 解決辦法2:dynamic_cast,要求待轉換的變數型別e存在多型  
 Player* p1 = dynamic_cast<Player*>(actuallyPlayer);  
    /* 將actuallyEnemy轉換為Player時會返回nullptr,表示轉換失敗 */ Player* p2 = dynamic_cast<Player*>(actuallyEnemy);  
  
    return 0;  
}

74.【Cherno C++】C++的基準測試

#include <iostream>
#include <chrono>

class Timer
{
public:
    Timer() {
        m_StartTimePoint = std::chrono::high_resolution_clock::now();
    }

    ~Timer() {
        Stop();
    }

private:
    std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimePoint;

    void Stop() {
        auto endTimePoint = std::chrono::high_resolution_clock::now();
        auto start = std::chrono::time_point_cast<std::chrono::nanoseconds>(
                m_StartTimePoint).time_since_epoch().count();
        auto end = std::chrono::time_point_cast<std::chrono::nanoseconds>(endTimePoint).time_since_epoch().count();
        auto nanoTime = end - start;
        std::cout << nanoTime << std::endl;
    }
};

int main() {
    int value = 0;
    {
        Timer timer;
        for (int i = 0; i < 1000000; i++)value += 2;
    }
    std::cout << value << std::endl;
}
  • 注意:程式碼需要在release模式下測試才有意義
    • 在Debug模式下:$2001600ns=2001.6\mu s=2.0016ms$
    • 在Release模式下:$0ns$
      • 該迴圈被跳過,$i$直接賦值為$2000000$

75.【Cherno C++】C++的結構化繫結

  • C++17標準新功能【適用於有多個返回值的情況】
#include <iostream>
#include <tuple>
#include <string>

std::tuple<std::string, int> CreatePerson() {
    return {"li", 25};
}

template<class T1, class T2>
void output(const T1& reference1, const T2& reference2) {
    std::cout << reference1 << ", " << reference2 << std::endl;
}

int main() {
    auto person = CreatePerson();
    // 在C++17之前,訪問tuple中元素非常麻煩,尤其是程式碼多的時候
    /* 0,1分別表示name和age,但程式碼量大時很難將索引與名稱一一對應 */
    auto name = std::get<0>(person);
    auto age = std::get<1>(person);

    /* 還可以使用std::tie,但name1和age1仍然需要事先定義 */
    std::string name1{};
    int age1{};
    std::tie(name1, age1) = CreatePerson();

    // C++17新特性,結構化繫結【語法糖】
    auto[name2, age2] = CreatePerson();

    // 結果輸出
    output(name, age);
    output(name1, age1);
    output(name2, age2);
    return 0;
}

76.【Cherno C++】如何處理OPTIONAL資料

  • C++17標準新功能std::optional
    • 處理可存在可不存在的資料【類似於C#中的可空型別】
#include <iostream>
#include <string>
#include <fstream>
#include <optional>

/* 很難知道檔案是否被開啟,返回空字串不能確定是沒找到還是檔案本身為空 */
std::string ReadFileAsString(const std::string& filepath) {
    std::ifstream stream(filepath);
    if (stream) {
        std::string result{};
        // read file[此處省略讀檔案的程式碼]
        stream.close();
        /* 讀取成功返回結果 */
        return result;
    }
    // 如果沒有成功開啟檔案,則返回預設字串【空字串】
    return {};
}

/* 需要額外的引用引數 */
std::string ReadFileAsString(const std::string& filepath, bool& isSuccess) {
    std::ifstream stream(filepath);
    if (stream) {
        std::string result{};
        stream.close();
        isSuccess = true;
        return result;
    }
    isSuccess = false;
    return {};
}

/* C++17新特性,std::optional,如果沒有資料返回null,如果有資料返回該資料 */
std::optional<std::string> ReadFileAsStringOptional(const std::string& filepath) {
    std::ifstream stream(filepath);
    if (stream) {
        std::string result{};
        stream.close();
        return result;
    }
    return {};
}

int main() {
    auto data = ReadFileAsStringOptional("data.txt");
    if (data.has_value()) std::cout << "File read successfully" << std::endl;
    else std::cout << "File read failed." << std::endl;

    /* 可以使用std::optional設定預設引數,如果使用者設定該引數則使用使用者值,如果沒設定則使用預設引數 */
    std::optional<int> count{};
    int c = count.value_or(100);
    std::cout << c << std::endl;
    c = 10;
    std::cout << c << std::endl;
}

77.【Cherno C++】單一變數存放多種型別的資料

  • C++17標準新功能std::variant
    • 不用擔心所處理資料的確切資料型別
      • 類似於C#中的dynamic,但是Cpp中的std::variant是半自動的。
        • 在使用之前需要手動給定一個型別表
        • 在型別表內的型別才能隨意切換
        • 在讀取時還需要使用std::get<T>()手動執行型別轉換
#include <iostream>
#include <variant>
#include <string>

int main() {
    std::variant<std::string, int> data;
    data = "li";
    std::cout << std::get<std::string>(data) << std::endl;
    data = 25;
    std::cout << std::get<int>(data) << std::endl;
    /* 在直接使用std::get<T>()的情況下,如果資料型別錯誤會引發異常 */
    /* 使用std::get_if<T>替代 */
    if (auto stringValue = std::get_if<std::string>(&data)) {
        std::string& v = *stringValue;
        // do something with string v.
    } else if (auto intValue = std::get_if<int>(&data)) {
        int& v = *intValue;
        // do something with int v.
    }
    
    return 0;
}

78.【Cherno C++】如何儲存任意型別的資料

  • C++17標準新功能std::any
    • 類似於std::variant,但不需要在定義時指定型別
    • 在使用std::any_cast<T>轉換變數時仍然要確保型別一致
    • 注:避免使用std::any,使用std::optional或std::variant替代
#include <iostream>
#include <any>
int main() {
    std::any data;
    data = 2;
    std::cout << std::any_cast<int>(data) << std::endl;
    data = "test";
    auto string = std::any_cast<const char*>(data);
    std::cout << string << std::endl;
}

82.【Cherno C++】C++的單例模式

  • 透過Get()訪問任意數量的非靜態成員方法
#include <iostream>

class Singleton
{
public:
    // 單例類不能被複制,所以刪除複製建構函式以避免複製
    Singleton(const Singleton&) = delete;

    static Singleton& Get() {
        return s_Instance;
    };

    void Function() {
        m_Member += 2.3f;
        std::cout << m_Member << std::endl;
    }

    void Function1() {
        m_Member -= 1.4f;
        std::cout << m_Member << std::endl;
    }

private:
    Singleton() = default;

    static Singleton s_Instance;
    float m_Member{};
};

Singleton Singleton::s_Instance;

int main() {
    Singleton::Get().Function();
    Singleton::Get().Function1();
}

82.1. 補充內容:隨機數生成方法

  1. linux下直接呼叫std::random_device,然而windows不行,windows下的隨機數週期明顯,不是真正的隨機數
#include <iostream>  
#include <random>  
#include <vector>  
  
int main() {  
    std::random_device rd;  
    std::vector<unsigned int> randomNumbers{};  
    int numbersOfRandomNumbers = 10;  
    randomNumbers.reserve(numbersOfRandomNumbers);  
    for (int i = 0; i < numbersOfRandomNumbers; i++) randomNumbers.push_back(rd());  
    for (auto& element: randomNumbers) std::cout << element << ", ";  
    std::cout << std::endl;  
    randomNumbers.clear();  
    for (int i = 0; i < numbersOfRandomNumbers; i++) randomNumbers.push_back(rd());  
    for (auto& element: randomNumbers) std::cout << element << ", ";  
    std::cout << std::endl;  
}
  1. 在windows和linux中獲取真隨機數的通用方法
/* 加入std::mt19937 mt(rd());以加強windows下隨機數的隨機性 */
#include <iostream>
#include <random>
#include <vector>

int main() {
    std::random_device rd;
    // 增強隨機性
    std::mt19937 mt(rd());
    std::vector<unsigned int> randomNumbers{};
    int numbersOfRandomNumbers = 10;
    randomNumbers.reserve(numbersOfRandomNumbers);
    for (int i = 0; i < numbersOfRandomNumbers; i++) randomNumbers.push_back(mt());
    for (auto& element: randomNumbers) std::cout << element << ", ";
    std::cout << std::endl;
    randomNumbers.clear();
    for (int i = 0; i < numbersOfRandomNumbers; i++) randomNumbers.push_back(mt());
    for (auto& element: randomNumbers) std::cout << element << ", ";
    std::cout << std::endl;
}

84.【Cherno C++】跟蹤記憶體分配的簡單方法

  • 過載new和delete運算子並使用智慧指標
#include <iostream>
#include <memory>

struct AllocationMetrics
{
    uint32_t TotalAllocated = 0;
    uint32_t TotalFreed = 0;

    // [[nodiscard]] 如果返回的物件被呼叫者丟棄,則編譯器將生成警告。
    [[nodiscard]] uint32_t CurrentUsage() const {
        return TotalAllocated - TotalFreed;
    }

    void PrintMemoryUsage() const {
        std::cout << "Memory Usage:" << this->CurrentUsage() << " bytes\n";
        std::cout << "TotalAllocated " << TotalAllocated << " bytes\n";
        std::cout << "TotalFreed " << TotalFreed << " bytes\n";
        std::cout << std::endl;
    }
};

static AllocationMetrics sg_AllocationMetrics;

void* operator new(std::size_t size) {
    sg_AllocationMetrics.TotalAllocated += size;
    return malloc(size);
}

void operator delete(void* memory, std::size_t size) {
    sg_AllocationMetrics.TotalFreed += size;
    free(memory);
}


struct Object
{
    int x, y, z;

    Object() = default;

    ~Object() = default;
};

int main() {
    sg_AllocationMetrics.PrintMemoryUsage();
    {
        //Allocating 12 bytes
        std::unique_ptr<Object> object = std::make_unique<Object>();
        sg_AllocationMetrics.PrintMemoryUsage();
    }//Freeing 12 bytes
    sg_AllocationMetrics.PrintMemoryUsage();
}

89.【Cherno C++】移動語義

#include <iostream>
#include <cstring>

class StringManually
{
public:
    StringManually() = default;

    explicit StringManually(const char* string) {
        printf("Created!\n");
        m_Size = strlen(string);
        m_Data = new char[m_Size];
        memcpy(m_Data, string, m_Size);
    }

    StringManually(const StringManually& other) {
        printf("Copied!\n");
        m_Size = other.m_Size;
        m_Data = new char[m_Size];
        memcpy(m_Data, other.m_Data, m_Size);
    }

    ~StringManually() {
        printf("Destroyed!\n");
        delete[] m_Data;
    }

    void Print() {
        for (uint32_t i = 0; i < m_Size; i++)printf("%c", m_Data[i]);
        printf("\n");
    }

private:
    char* m_Data{};
    uint32_t m_Size{};
};

class Entity
{
public:
    explicit Entity(const StringManually& name)
            : m_Name(name)/* 在此處複製了一次,name複製給Entity建立的StringManually例項m_Name */ {
    }

    void PrintName() {
        m_Name.Print();
    }

private:
    StringManually m_Name{};
};

int main() {
    // 有了一次不必要的複製過程,無法把StringManually("Che")這一例項直接傳入Entity的m_Name中
    Entity entity(StringManually("Che"));
    entity.PrintName();
}
  • 使用移動建構函式代替複製建構函式
#include <iostream>
#include <cstring>

class StringManually
{
public:
    StringManually() = default;

    explicit StringManually(const char* string) {
        printf("Created!\n");
        m_Size = strlen(string);
        m_Data = new char[m_Size];
        memcpy(m_Data, string, m_Size);
    }

    // 禁止複製
    StringManually(const StringManually&) = delete;

    // 移動建構函式
    /* noexcept用於約定該函式不會丟擲異常,如果丟擲異常則直接終止程式,
     * 其作用在於阻止異常的傳播和擴散 */
    StringManually(StringManually&& other) noexcept {
        printf("Moved!\n");
        m_Size = other.m_Size;
        m_Data = other.m_Data;
        // 在移動建構函式中要處理舊的StringManually例項other
        other.m_Size = 0;
        other.m_Data = nullptr;
    }

    ~StringManually() {
        printf("Destroyed!\n");
        delete[] m_Data;
    }

    void Print() {
        for (uint32_t i = 0; i < m_Size; i++)printf("%c", m_Data[i]);
        printf("\n");
    }

private:
    char* m_Data{};
    uint32_t m_Size{};
};

class Entity
{
public:
    // 使用右值引用建構函式替代傳值的建構函式
    explicit Entity(StringManually&& name)
            : m_Name((StringManually&&) name) {
        /* 2. Entity例項在初始化過程中由m_Name接收了傳入的右值,沒有複製過程
         * 省去了不必要的複製操作。*/
        std::cout << "Entity Created with Entity(StringManually&& name)." << std::endl;
    }
    /* 3. StringManually("Che")建立了匿名StringManually例項經name傳入Entity後失去作用,被析構
     * 注:name對應於移動建構函式中的StringManually(StringManually&& other) noexcept */

    void PrintName() {
        m_Name.Print();
    }

    ~Entity() {
        std::cout << "Entity Destroyed." << std::endl;
    }

private:
    StringManually m_Name{};
};

int main() {
    /* 1. StringManually("Che")建立了匿名StringManually例項 */
    Entity entity(StringManually("Che"));
    entity.PrintName();
    /* 4-5.在析構Entity的過程中析構StringManually m_Name */

    /* 總結:Reason for two destroys: 1 where the hollow object is destroyed + 1 where the actual heap memory is deallocated. */
    /* 總結:The first one is destroying object with size of 0 and data as null */
    /* 總結:兩次銷燬的原因:
     * 1. 空物件【StringManually(StringManually&& other)中的other】被銷燬(虛假銷燬,釋放的記憶體是nullptr指向的記憶體)
     * 2. 實際堆記憶體被釋放。 */
}

90. 建構函式、複製建構函式、移動建構函式總結

注意:即使Test是模板類Test<T>,Test<T>的複製和移動建構函式仍然和非泛型的Test類相同。考慮如下示例:

template<typename T>
class A
{
public:
    A(const A& theOther) = default;

    A& operator=(const A& theOther) = default;

    A(A&& theOther) noexcept = default;

    A& operator=(A&& theOther) noexcept = default;

};

在上述示例中,即使A含有模板引數,但在類A的內部將A作為型別使用時,它也會被認為是完整的型別A<T>

#include <iostream>
#include <string>
#include <utility>

class Test
{
public:
    Test() {
        std::cout << "constructor" << std::endl;
        std::cout << GetStrings() << std::endl;
    };

    explicit Test(std::string strings) : _strings(std::move(strings)) {
        std::cout << "constructor" << std::endl;
        std::cout << GetStrings() << std::endl;
    }

    Test(const Test& others) : _strings(others._strings) {
        std::cout << "copy constructor" << std::endl;
        std::cout << GetStrings() << std::endl;
    };

    Test& operator=(const Test& others) = delete;

    Test(Test&& others) noexcept: _strings(std::move(others._strings)) {
        std::cout << "move constructor" << std::endl;
        std::cout << GetStrings() << std::endl;
    };

    Test& operator=(Test&& newEntity) = delete;

    ~Test() = default;

    std::string& GetStrings() {
        return _strings;
    }

    void ModifyStrings(const std::string& newString) {
        _strings = newString;
        std::cout << "string has been modified to " << _strings << std::endl;
    }

private:
    std::string _strings;
};

int main() {
    auto* test1 = new Test("test1");
    auto* test2 = new Test("test2");
    delete test2;
    test2 = new Test(*test1);
    test2->ModifyStrings("Test2");
    delete test2;
    test2 = new Test(std::move(*test1));
    test2->ModifyStrings("Test2");
    delete test2;

    Test test3("test3");
    Test test4(test3);
    test4.ModifyStrings("Test4");
    Test test5(std::move(test4));
    test5.ModifyStrings("Test5");
}

執行結果如下:

constructor
test1
constructor
test2
copy constructor
test1
string has been modified to Test2
move constructor
test1
string has been modified to Test2
constructor
test3
copy constructor
test3
string has been modified to Test4
move constructor
Test4
string has been modified to Test5

相關文章