1. 程式人生 > 實用技巧 >百度翻譯

百度翻譯

由於learncpp.com內容過多,此篇部落格記錄後半部分,前半部分請移步我的部落格c++筆記1(參考learncpp.com)

1、函式傳參的3種方式引用實現多返回值函式

3種傳參方式:值傳遞、引用傳遞、地址傳遞。

單個基本資料型別,用值傳遞。
其他型別(string、array等)用引用傳遞,若不想引數被更改,用const修飾。

引用、指標需要注意的是函式引數必須為左值(如i=5,i是左值,5是右值),因為這兩者有“地址”觀念。

int foo1(int x);    // pass by value
int foo2(int &x);    // pass by reference
int
foo3(int *x); // pass by address int i {}; foo1(i); // i不被更改,因為有拷貝 foo2(i); // i可能被改,若不想被改,用int foo2(const int &x); foo3(&i); // i可能被改

引用實現多返回值函式

#include <iostream>
#include <cmath>    // for std::sin() and std::cos()
 
void getSinCos(double degrees, double &sinOut, double &cosOut)
{
    
static constexpr double pi { 3.14159265358979323846 }; // the value of pi double radians = degrees * pi / 180.0; //度轉為弧度,如30°→Π/6 sinOut = std::sin(radians); cosOut = std::cos(radians); } int main() { double sin(0.0); double cos(0.0); getSinCos(30.0, sin, cos); std::cout << "
The sin is " << sin << '\n'; std::cout << "The cos is " << cos << '\n'; return 0; }

另外可以參考我的另外一篇部落格,函式間引數傳遞的3種方式

2、函式多返回值的3種實現方式

方式一:引用,參考上節內容。OpenCV影象處理框架中常見此用法。

方式二:結構體。

#include <iostream>

struct S
{
    int m_x;
    double m_y;
};
S returnStruct() //返回結構體
{
    S s;
    s.m_x = 5;
    s.m_y = 6.7;
    return s;
}

int main()
{
    S s{ returnStruct() };
    std::cout << s.m_x << ' ' << s.m_y << '\n';

    return 0;
}

方式三:元組 std::tuple。

#include <tuple>
#include <iostream>

std::tuple<int, double> returnTuple() // 返回元組
{
    return { 5, 6.7 };
}

int main()
{
    std::tuple s{ returnTuple() }; // 呼叫函式
    std::cout << std::get<0>(s) << ' ' << std::get<1>(s) << '\n'; // 用std::get<n>獲取元組中元素
    /*或者使用如下方式
    int a;
    double b;
    std::tie(a, b) = returnTuple(); // 用std::tie拆解元組
    //auto [a,b]{returnTuple()}; // c++17,效果同上,拆解元組
    std::cout << a << ' ' << b << '\n';
    */
    return 0;
}

3、指向函式地址的指標,std::function的使用

功能:
① 函式指標主要用於在陣列(或其他結構)中儲存函式,
② 在需要將函式(此函式又稱回撥函式)傳遞給另一個函式時。
因為宣告函式指標的本機語法很難看而且容易出錯,所以我們建議使用std::function。

嚴格按以下格式定義:
int (*fcnPtr)(); //指標fcnPtr是指向“無參且返回int型”函式的指標,fcnPtr可指向任何同類型的函式。
int (*const fcnPtr)(); //const函式

如下3種等效,fcnPtr指向“兩個引數int、double型且返回int型”的函式
int (*fcnPtr)(int,double);
std::function<int(int,double)> fcnPtr; //<返回型別(每個引數型別)>
auto fcnPtr;

#include <iostream>
// #include <functional> //for std::function

int foo(){    return 5;}
int goo(){    return 6;}

void main()
{
    int(*fcnPtr)(){&foo}; //指標只能指向“無參且返回int型”的函式
    //auto fcnPtr{ &foo }; //使用auto關鍵字,同上等效
    //std::function<int()> fcnPtr{ &foo }; //使用std::function,同上等效
    fcnPtr=&goo; //指向函式goo的地址,不需要()
    std::cout << fcnPtr(); //隱式使用,更簡潔
    //std::cout << (*fcnPtr)(); //顯式使用,同上等效
}

注意,帶預設引數的函式,必須顯式地傳遞任何預設引數的值。預設引數是在編譯時解析的,而函式指標是在執行時解析的。因此,當使用函式指標進行函式呼叫時,無法解析預設引數。

【函式作為引數實現升序、降序】

#include <utility> // for std::swap
#include <iostream>

// 作為引數的函式,又稱回撥函式,含兩個int引數、返回bool。ascending、descending兩個回撥函式。
void selectionSort(int *array, int size, bool(*comparisonFcn)(int, int))
{
    // 前 n-1個元素,與之後的比較大小
    for (int startIndex{ 0 }; startIndex < (size - 1); ++startIndex)
    {
        int bestIndex{ startIndex }; //儲存最大或最小元素的下標
        // 後n-1個元素
        for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
        {            
            if (comparisonFcn(array[bestIndex], array[currentIndex])) // 使用回撥函式作為判斷條件
            {
                bestIndex = currentIndex; // 儲存最大或最小元素的下標
            }
        }

        // 交換元素,之前是交換下標
        std::swap(array[startIndex], array[bestIndex]);
    }
}

// 回撥函式,bool型別當作觸發升序的“開關”
bool ascending(int x, int y)
{
    return x > y; // 前>後則返回true,true則觸發交換
}

// 回撥函式
bool descending(int x, int y)
{
    return x < y; 
}

void printArray(int *array, int size) //陣列會退化為指標,丟失長度資訊,所以顯式指定
{
    for (int index{ 0 }; index < size; ++index)
    {
        std::cout << array[index] << ' ';
    }
    std::cout << '\n';
}

int main()
{
    int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };
    // 降序
    selectionSort(array, 9, descending);
    printArray(array, 9);
    // 升序
    selectionSort(array, 9, ascending);
    printArray(array, 9);

    return 0;
}

4、5個記憶體區域(又稱為段)

The code segment (also called a text segment),程式碼段(也稱為文字段),編譯後的程式位於記憶體中,程式碼段通常是隻讀的。
The bss segment (also called the uninitialized data segment),bss段(也稱為未初始化資料段),用於儲存未初始化的全域性變數和靜態變數。
The data segment (also called the initialized data segment),資料段(也稱為初始化的資料段),用於儲存初始化的全域性變數和靜態變數。
The heap,堆段,從堆中動態分配的變數。
The call stack,呼叫堆疊,其中儲存函式引數、區域性變數和其他函式相關資訊。

5、std::vector的堆疊操作(Stack behavior)

Stack 是後進先出的結構(LIFO),如果你在堆疊頂部放一個新盤子,從堆疊中移出的第一個盤子將會是你最後推入的盤子。當專案被推入堆疊時,堆疊會變得更大。當專案被彈出時,堆疊會變得更小。

【容量與長度】
int *array{ new int[10] { 1, 2, 3, 4, 5 } }; //容量是10,長度是5。
容量會依據長度自動擴容,但未必會隨著長度縮小。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> array{ 0, 1, 2 };
    std::cout << "length: " << array.size() << "  capacity: " << array.capacity() << '\n';
    array.resize(5); // 長度變為5,元素變為0,1,2,0,0
    std::cout << "length: " << array.size() << "  capacity: " << array.capacity() << '\n';
    array.resize(2); // 長度變為2,元素只剩0,1
    std::cout << "length: " << array.size() << "  capacity: " << array.capacity() << '\n';
}

【堆疊操作】

push_back()   將元素壓入堆疊。堆疊變大。
back()     返回堆疊頂部元素的值。堆疊不變,只是看最頂部的元素。
pop_back()  從堆疊中彈出一個元素。堆疊變小。

std::vector<int> stack{};
stack.push_back(5); 
stack.push_back(3);
stack.push_back(2); //stack中元素5,3,2
std::cout << "top: " << stack.back() << '\n'; // 2
stack.pop_back(); //拿走最頂部的2,stack中元素剩5,3

最終stack容量為3,長度為2

6、遞迴與迭代(Recursive vs iterative)

遞迴就是函式自己呼叫自己,迭代就是常見的for、while迴圈遍歷。

迭代效率高於遞迴,因為遞迴完成前、後,會推入和取出堆疊幀,都會產生一些開銷。

如下圖所示,每遞迴一次,count被int一次,新開闢一次記憶體,這些記憶體在遞迴完成後,需要釋放掉。stack棧是後進先出的。

程式碼:

#include <iostream>

void countDown(int count)
{
    std::cout << "push " << count << '\n';

    if (count > 1) // 終止條件
        countDown(count - 1); //遞迴前、後會推入和取出堆疊幀。因為引數count會被int很多次,即有新記憶體,這些新記憶體需要釋放掉。

    std::cout << "pop " << count << '\n';
}

int main()
{
    countDown(5);
    return 0;
}

7、assert 與 static_assert

assert語句是一個前處理器巨集,它在執行時計算條件表示式。
static_assert的條件部分必須能夠在編譯時計算。因為static_assert不是在執行時計算的,所以static_assert語句也可以放在程式碼檔案中的任何位置(甚至在全域性空間中)。

如果條件表示式為真,則assert語句不執行任何操作。
若為false,則顯示一條錯誤訊息並終止程式。此錯誤訊息包含失敗的條件表示式,以及程式碼檔案的名稱和斷言的行號。
斷言會損耗一點效能,一般Debug時使用,Release時關閉。IDE一般預設設定了此功能(即巨集NDEBUG)

【讓斷言具有描述性】
assert(found && "你的描述文字"); 或 static_assert(found, "你的描述文字");
原理:"你的描述文字"永遠為true,若found為false則觸發斷言,字串也會輸出出來。

【注意】
exit()函式和assert()函式(如果觸發)會立即終止程式,而沒有機會做任何進一步的清理(例如關閉檔案或資料庫)。因此,應該明智地使用它們(僅在程式意外終止而不太可能發生損壞的情況下使用)。

#include <iostream>
#include <cassert> //for assert()

int main()
{
    const int i{ 1 }; //若無const,則i在編譯時是未知的,執行以後才知道是1
    static_assert(i < 0, "必須是負數"); //條件必須是編譯時已知的

    int j{ 1 };
    assert(i < 0 && "必須是負數"); //執行時
    
    return 0;
}

8、Lambda

F.7.15與F.7.16章節暫時擱置,後期再回來更新。

https://www.learncpp.com/cpp-tutorial/introduction-to-lambdas-anonymous-functions/

https://www.learncpp.com/cpp-tutorial/lambda-captures/

9、struct與class

在C++中,對只有資料的物件使用struct關鍵字,對既有資料又有函式的物件使用class關鍵字。

因為,類會清理自己的記憶體是合理的(例如,一個分配記憶體的類會在銷燬之前釋放記憶體),但一個結構這樣做就不安全了。

注意,struct成員預設public,class成員預設private。

一般將class成員變數設定為私有,成員函式設定為公有,除非您有充分的理由不這樣做。

struct DateStruct //類與結構體,在純資料時幾乎無差別。
{
    int year{};
    int month{};
    int day{};
};
 
class DateClass
{
public:
    int m_year{};
    int m_month{};
    int m_day{};
};

10、類與類的關係——Composition組合

如,遊戲中,生物有名稱、位置這2個屬性,位置可以單獨拿出來作為類,成為生物類的一部分。

生物消亡,位置也會消亡。即整體負責區域性的釋放。

Point2D.h 簡單的函式實現直接寫標頭檔案裡了,複雜的可以寫對應的cpp裡。

#pragma once

#include<iostream>

class Point2D
{
private:
    int m_x;
    int m_y;

public:
    Point2D():m_x{0},m_y{0} //預設構造
    {
    }
    Point2D(int x, int y) :m_x{ x }, m_y{ y } //含參構造
    {
    }
    void setPoint(int x, int y) //訪問函式set、get
    {
        m_x = x;
        m_y = y;
    }
    int getX () const
    {
        return m_x;
    }
    int getY() const
    {
        return m_y;
    }
};

Creature.h

#pragma once

#include<string>
#include"Point2D.h"
class Creature
{
private:
    std::string m_name;
    Point2D m_location; //點是生物的一部分,生物負責點的消亡。
public:
    Creature(std::string name, Point2D location) :m_name{ name }, m_location{ location }
    {
    }
    void moveTo(int x, int y) //生物只需負責運動到哪,無需為建立點擔憂
    {
        m_location.setPoint(x, y);
    }
    void printMsg()
    {
        std::cout << m_name << " is at" << '(' << m_location.getX() << ',' << m_location.getY() << ')' << '\n';
    }
};

main.cpp

#include <iostream>
#include"Creature.h"
#include"Point2D.h"

int main()
{
    std::cout << "Enter a name\n";
    std::string name;
    std::cin >> name;
    Creature creature{ name,{4,7} };
    creature.printMsg();
    creature.moveTo(5, 8);
    creature.printMsg();
    return 0;
}

11、類與類的關係——Aggregation聚合

Aggregation聚合,整體不負責區域性的建立與釋放。區域性可以在同一時刻屬於不同的整體。

Composition組合,偏向於物件的固有屬性,物件不存在,屬性也就無意義了(消亡)。

人——出生日期,Composition組合

人——國家,Aggregation聚合

聚合可能更危險,因為聚合不處理其部分的分配。分配由外部方完成。如果外部方不再有指向廢棄部分的指標或引用,或者它只是忘記做清理(假設類會處理),那麼記憶體將會洩漏。

12、類與類的關係——Association關聯

物件之間屬於弱關係,可單向/雙向,各自獨立。如病人與醫生。

通常,應該避免雙向關聯,因為它們增加了複雜性,而且往往難以不出錯地編寫。

13、類與類的關係——Dependencies依賴

當一個物件為了完成某些特定任務而呼叫另一個物件的功能時,就會發生依賴。這是一種比關聯更弱的關係,但是,對所依賴的物件的任何更改都可能破壞(依賴的)呼叫者的功能。依賴關係始終是單向關係。

依賴關係通常不在類級別表示——也就是說,所依賴的物件沒有作為成員連結。相反,所依賴的物件通常在需要時例項化(比如開啟檔案向其寫入資料),或者作為引數傳遞到函式中。

關聯是類級別上兩個類之間的關係。也就是說,一個類將關聯類的直接或間接“連結”作為成員儲存。例如,Doctor類有一個指向其患者的指標陣列作為成員。

14、類與類的關係——Inheritance繼承

類與類間滿足is-a,就可以使用繼承。如蘋果is-a水果。

直接在派生類的初始化列表中賦值基類的成員變數是無效的,可以在初始化列表中使用基類建構函式,其位置並不重要——它總是首先執行。
派生類不能直接訪問基類的私有成員,可以使用訪問函式來訪問。

#include <iostream>
#include <string>

class Base
{
public:
    Base(int id=0)
        :m_id{id} //建構函式,初始化成員變數
    {}
    int getId() const{ return m_id; } //訪問函式
    void setId(int temp) { m_id = temp; }
private:
    int m_id;
};

class Derived :public Base
{
public:
    Derived(double cost=0.0)
        :m_cost{cost}
    {}
    double getCost() const{ return m_cost; } //訪問函式

private:
    double m_cost;
};

int main()
{
    Derived derived{1.3};
    std::cout << derived.getCost() << '\n';
    derived.setId(10); //通過基類的訪問函式修改基類的成員變數
    std::cout << derived.getId() << '\n';

    return 0;
}

也可以在派生類的初始化列表中使用基類的建構函式,賦值基類的成員變數

#include <iostream>
#include <string>

class Base
{
public:
    Base(int id=0)
        :m_id{id} //建構函式,初始化成員變數
    {}
    int getId() const{ return m_id; } //訪問函式
    //void setId(int temp) { m_id = temp; }
private:
    int m_id;
};

class Derived :public Base
{
public:
    Derived(double cost=0.0,int id=0)
        :m_cost{cost},
        Base{id} //m_id{id}無效,Call Base(int) constructor with value id!
    {}
    double getCost() const{ return m_cost; } //訪問函式

private:
    double m_cost;
};

int main()
{
    Derived derived{1.3,10}; //省去了derived.setId(10);
    std::cout << derived.getCost() << '\n';
    std::cout << derived.getId() << '\n';

    return 0;
}

15、公有繼承、基類成員的訪問說明符

繼承時推薦公有繼承,其他繼承參考標題連結。

任何人都可以訪問公共成員。
受保護的基類成員可以被派生類直接訪問,但不能被公眾訪問。
私有的基類成員不可被派生類、公眾訪問。

class Base
{
public: //公有成員
    int m_public;
protected: //受保護成員
    int m_protected;
private: //私有成員
    int m_private;
};

class Derived : public Base //派生類,公有繼承
{
public:
    Derived()
    {
        m_public = 1; // 類內可訪問
        m_protected = 2; // 類內可訪問
        m_private = 3; // 類內不可訪問
    }
};

int main()
{
    Base base; //Derived derived同理
    base.m_public = 1; // 類外可訪問
    base.m_protected = 2; // 類外不可訪問
    base.m_private = 3; // 類外不可訪問
}

16、基類指標

基類指標等效於基類::,即使指向其他類,呼叫的仍是基類的成員。

即,基指標或引用只能呼叫函式的基版本,而不能呼叫派生版本。

#include<iostream>
#include<string_view>
#include<string>

class Animal
{
public:
    Animal(const std::string &name)
        :m_name{name}
    {}
    const std::string &getName() const{return m_name;}
    
    std::string_view speak() const{return "???";}
    
private:
    std::string m_name;
};

class Cat:public Animal
{
public:
    Cat(const std::string &name)
        :Animal{name} //派生類初始化基類的私有成員變數
    {}
    std::string_view speak() const {return "Meow";}
};

int main()
{
    Cat cat{"Tom"}; //基類的私有成員變數被初始化為Tom
    std::cout<<cat.getName()<<" "<<cat.speak()<<'\n';//Tom Meow

    Animal *p{&cat}; //因為p是Animal基類的指標,所以p->是Animal::而不是Cat::
    std::cout<<p->getName()<<" "<<p->speak()<<'\n'; //Tom ???

    return 0;
}

如下是一種讓cat發出Meow而不是???的方法,與上述沒有本質區別,基類中增加m_speak成員變數,在派生類中初始化它。

#include<iostream>
#include<string_view>
#include<string>

class Animal
{
public:
    Animal(const std::string &name,std::string_view speak)
        :m_name{name},m_speak{speak}
    {}
    const std::string &getName() const{return m_name;}
    std::string_view speak() const { return m_speak; }

private:
    std::string m_name;
    std::string_view m_speak;
};

class Cat:public Animal
{
public:
    Cat(const std::string &name)
        :Animal{name,"Meow"} //派生類初始化基類的私有成員變數
    {}
};

int main()
{
    Cat cat{"Tom"};
    Animal *p{&cat}; //p->依然是Animal::而不是Cat::
    std::cout<<p->getName()<<" "<<p->speak()<<'\n'; //Tom Meow

    return 0;
}

17、虛擬函式(c++中重量級內容)

為了解決“基指標或引用只能呼叫函式的基版本,而不能呼叫派生版本”問題,虛擬函式閃亮登場。

虛擬函式是一種特殊型別的函式,在呼叫時,它解析為存在於基類和派生類之間的函式的最終派生版本。這種能力稱為多型性。
如果派生函式具有與基版本函式相同的簽名(名稱、引數型別以及是否為常量)和返回型別,則認為該派生函式是匹配的,這樣的函式稱為覆蓋(重寫)。

#include<iostream>
#include<string_view>
#include<string>

class Animal
{
public:    
    virtual std::string_view speak() const{return "???";} //加上virtual關鍵字
};

class Cat:public Animal
{
public:
    virtual std::string_view speak() const {return "Meow";} //加上virtual關鍵字
};

int main()
{
    Cat cat;
    Animal *p{&cat};
    std::cout<<p->speak()<<'\n'; //Meow,如果上述不加virtual,則???

    return 0;
}

通常解析為Animal::speak()。但是,Animal::speak()是虛擬的,它告訴程式去檢視基函式和派生函式之間是否有更多派生版本可用,有則用派生版本。在本例中,它將解析為派生的::speak()。

基類、派生類中,虛擬函式的名稱、引數、返回型別必須完全一致。否則即使virtual修飾,也認為是獨立的函式。

若基類中函式是虛的,那麼派生類中預設此函式也是虛的,不過用virtual修飾是一種好習慣。

class Base //這兩個函式是獨立的
{
public:
    virtual int getValue() const { return 5; }
};
 
class Derived: public Base
{
public:
    virtual double getValue() const { return 6.78; }
};

18、為虛擬函式而生的override、final關鍵字

不加override,虛擬函式也可以執行,但是對於不是真正的虛擬函式(認為重寫了,其實沒有),編譯器不提醒錯誤。

class A
{
public:
    virtual const char* getName1(int x) { return "A"; }
    virtual const char* getName2(int x) { return "A"; }
    virtual const char* getName3(int x) { return "A"; }
};
 
class B : public A
{
public:
    virtual const char* getName1(short int x) override { return "B"; } // compile error, function is not an override
    virtual const char* getName2(int x) const override { return "B"; } // compile error, function is not an override
    virtual const char* getName3(int x) override { return "B"; } // okay, function is an override of A::getName3(int)
 
};
 
int main()
{
    return 0;
}

若禁止虛擬函式被重寫或類被繼承,使用final說明符。若使用者試圖覆蓋已指定為final的函式或類,編譯器將給出編譯錯誤。

class A
{
public:
    virtual const char* getName() { return "A"; }
};
 
class B : public A
{
public:
    virtual const char* getName() override final { return "B"; } // okay, overrides A::getName()
};
 
class C : public B
{
public:
    virtual const char* getName() override { return "C"; } // compile error: overrides B::getName(), which is final
};

類不可被繼承

class A
{
public:
    virtual const char* getName() { return "A"; }
};
 
class B final : public A // note use of final specifier here
{
public:
    virtual const char* getName() override { return "B"; }
};
 
class C : public B // compile error: cannot inherit from final class
{
public:
    virtual const char* getName() override { return "C"; }
};

19、虛析構

在17節中講到,基類、派生類中,虛擬函式的名稱、引數、返回型別必須完全一致。否則即使virtual修飾,也認為是獨立的函式。

但有特殊情況,協變返回型別

#include <iostream>

class Base
{
public:
    virtual Base* getThis() { std::cout << "called Base::getThis()\n"; return this; } //返回指向Base類的指標
    void printType() { std::cout << "returned a Base\n"; }
};

class Derived : public Base
{
public:
    // 通常,重寫,返回型別必須一致,但是Derived繼承自Base, 允許寫成Derived*,真正返回的依然是Base*
    virtual Derived* getThis() override { std::cout << "called Derived::getThis()\n";  return this; }
    void printType() { std::cout << "returned a Derived\n"; }
};

int main()
{
    Derived d;
    Base* b = &d;
    d.getThis()->printType(); // 呼叫Derived::getThis(), 返回Derived*, 呼叫Derived::printType
    b->getThis()->printType(); // 呼叫Derived::getThis(), 返回Base*, 呼叫Base::printType

    return 0;
}

b->本質是Base::,但Base :: getThis()是虛擬函式,因此呼叫Derived :: getThis()。

儘管Derived :: getThis()返回Derived *,Derived *會被向上轉換為基版本Base *,因此,將呼叫Base :: printType()。

虛析構也如此,在處理繼承時,應該將任何顯式解構函式設為虛擬函式

#include <iostream>
class Base
{
public:
    virtual ~Base() // note: virtual
    {
        std::cout << "Calling ~Base()\n";
    }
};

class Derived : public Base
{
private:
    int* m_array;

public:
    Derived(int length)
        : m_array{ new int[length] }
    {   }

    virtual ~Derived() // 基函式虛,預設也虛。如果~Base()不是虛擬函式,則認為此函式沒有重寫~Base()
    {
        std::cout << "Calling ~Derived()\n";
        delete[] m_array;
    }
};

int main()
{
    Derived* derived{ new Derived(5) };
    Base* base{ derived };

    delete base; //若~Base()非虛,則只執行~Base()。否則,先~Derived()再~Base()

    return 0;
}

base是一個Base*,當base被刪除時,程式會檢視~Base()是否為虛擬函式。若不是,則不會再找其他版本,直接執行~Base()。若是,則執行~Derived()再執行~Base()。

20、純虛擬函式(抽象函式)、抽象基類、介面類

純虛擬函式使得基類不能被例項化,派生類被迫在例項化這些函式之前定義這些函式。這有助於確保派生類不會忘記重新定義基類所期望的函式。
virtual int getValue() const = 0; //純虛擬函式格式

① 任何具有一個或多個純虛擬函式的類會變為抽象基類,抽象基類不能例項化(因為其中的純虛擬函式它不知道要幹什麼)。
② 任何派生類都必須為這個函式定義一個主體,否則派生類也將被視為一個抽象基類。

如動物類都會有叫聲,具體的派生類才知道(實現)具體的叫聲。
virtual const char* speak() = 0; // class Animal
const char* speak() const override { return "Moo"; } //class Cow: public Animal

【介面類】

介面類是沒有成員變數的類,其中所有的函式都是純虛的。
換句話說,這個類純粹是一個定義,沒有實際的實現。
注意,類中要有虛析構。