C++溫故補缺(二十一):雜項補充2

2023-06-01 18:00:21

雜記2

explicit

在 C++ 中,explicit 是一個關鍵字,用於修飾類別建構函式,其作用是禁止編譯器將一個引數建構函式用於隱式型別轉換。具體來說,當一個建構函式被 explicit 修飾時,只能通過顯式呼叫來建立該類的物件,而不能通過隱式型別轉換來建立物件。

下面通過一個例子來說明 explicit 關鍵字的作用:

class A {
public:
    explicit A(int x) : m_x(x) {}
    int getX() const {
        return m_x;
    }

private:
    int m_x;
};

void foo(A a) {
    cout << a.getX() << endl;
}

int main() {
    A a1{1}; // 正確,使用顯式呼叫建構函式建立物件
    A a2 = 2; // 錯誤,隱式型別轉換被禁止了
    foo(3); // 錯誤,隱式型別轉換被禁止了
    foo(A{4}); // 正確,使用顯式呼叫建構函式建立物件
    return 0;
}

在上面的程式碼中,我們定義了一個類 A,其中建構函式被 explicit 修飾。我們在函數 foo 中使用了 A 型別的引數,然後在 main 函數中分別使用了顯式呼叫建構函式和隱式型別轉換來建立 A 型別的物件,並嘗試將這些物件作為引數傳遞給函數 foo。

由於建構函式被 explicit 修飾,因此我們無法直接使用隱式型別轉換來建立 A 型別的物件,也無法將一個整數隱式轉換為 A 型別並作為引數傳遞給函數 foo。只有通過顯式呼叫建構函式來建立物件,才能正常進行編譯。

使用 explicit 關鍵字可以顯式地指定哪些建構函式可以被用於隱式型別轉換,從而避免了一些潛在的型別轉換錯誤。使用 explicit 關鍵字還可以提高程式碼的可讀性,使得程式碼更加易於理解和維護。

export

export關鍵字是C++20中引入的新特性,用於向編譯器宣告和匯出模組介面。它用於定義模組的介面,可以將宣告的識別符號(函數、變數、類等)匯出到模組外部,供其他模組使用。同時,它還可以限定一個模組內的識別符號只能被本模組內的其他程式碼使用,而不能被匯出到外部。

export關鍵字通常與import關鍵字一起使用,import用於匯入其他模組的介面。

下面是一個使用export的範例:

// module1.cpp 
export module module1;

export int add(int a, int b) {
    return a + b;
}

export int value = 42;

// main.cpp
import module1;

int main() {
    int result = add(2, 3);
    std::cout << "result: " << result << std::endl;
    std::cout << "value: " << value << std::endl;
    return 0;
}

在上面的範例中,module1模組中匯出了add函數和value變數,並在main.cpp中使用了這兩個識別符號。當編譯main.cpp時,編譯器會自動將module1模組編譯成一個單獨的檔案,並將其連結到main程式中。

需要注意的是,export關鍵字目前還不是所有編譯器都支援,具體情況需要檢視編譯器的檔案。

typeid和decltype

C++中的typeid關鍵字用於獲取一個表示式的型別資訊,可以用來判斷兩個物件是否為同一種型別。具體來說,typeid可以返回一個type_info型別的物件,該物件包含了表示式的型別資訊,可以通過type_info物件的name()方法獲取型別名。typeid一般用於執行時型別識別(RTTI)。

RTTI執行時型別識別(Runtime Type Identification,RTTI)是一種在程式執行時確定物件型別的機制。RTTI通常用於C++中,可以使用typeid運運算元來獲得物件的型別資訊。在程式中,通過將物件轉換為其基礎類別或介面型別來使用RTTI。RTTI可以用於在程式執行時進行型別安全的轉換和例外處理,但過度使用RTTI可能會導致程式碼的效能下降。

與之相關的還有編譯時型別識別(Compile-time type checking)

例如,下面的程式碼可以獲取一個變數的型別資訊,並輸出其型別名:

#include <iostream>
#include <typeinfo>

int main() {
    int x = 42;
    const std::type_info& type = typeid(x);
    std::cout << type.name() << std::endl;
    return 0;
}

輸出結果為:

i

這裡的i表示整數型別。另外,char型的型別值為c,long型的型別值為l,float型為f。簡單型別是用的單個字元表示,類或函數用字串表示,其中會包括返回值型別、引數型別等字元,如:

int fun(long x=0,char y='1'){
    return 0;
}
int main(){
    const std::type_info& type = typeid(fun);
    std::cout << type.name() << std::endl;
}

第二個i是函數返回值型別,l是第一個引數型別值,c是第二個引數型別值。

另外,C++11中引入了decltype關鍵字,可以用來獲取一個表示式的型別,也可以用於函數返回型別的推導等。

C++11中引入了decltype關鍵字,用於獲取一個表示式的型別,也可以用於函數返回型別的推導等。decltype的基本語法如下:

decltype(expression)

其中,expression可以是任意一個表示式,decltype會返回該表示式的型別,包括const、參照等修飾符。

下面是幾個使用decltype的例子:

int x = 42;
const int& y = x;
decltype(x) z1 = 0;  // z1的型別為int
decltype(y) z2 = x;  // z2的型別為const int&
decltype(x+y) z3 = 0;  // z3的型別為int

在上面的程式碼中,z1的型別為int,因為decltype(x)的結果是intz2的型別為const int&,因為decltype(y)的結果是const int&z3的型別為int,因為decltype(x+y)的結果是int

另外,decltype還可以用於函數返回型別的推導。例如:

template <typename T, typename U>
auto add(T t, U u) -> decltype(t+u) {
    return t + u;
}

在上面的程式碼中,decltype(t+u)用於推導函數返回型別,即tu相加的結果型別。

需要注意的是,decltype並不會對錶示式進行求值,而是僅僅返回表示式的型別。因此,如果表示式中包含了函數呼叫或運運算元過載等操作,decltype會返回對應的函數返回型別或運運算元過載結果型別,而不是表示式的值。

typename

typename關鍵字則用於指明一個名稱是型別名稱。在C++中,有時需要使用巢狀的型別名稱,如類別範本中的型別別名或巢狀類的名稱等。在這種情況下,需要使用typename關鍵字來指明名稱是型別名稱而非成員名稱。例如:

template <typename T>
struct my_template {
    typename T::value_type* ptr;
};

在上面的程式碼中,typename T::value_type表示T型別中的value_type型別,而不是T型別中的名為value_type的成員變數。如果省略了typename關鍵字,則編譯器會將T::value_type解釋為成員變數名,從而導致編譯錯誤。

四種cast

在C++中,有四種型別轉換的方式,它們分別是static_cast、dynamic_cast、const_cast和reinterpret_cast。

  1. static_cast:用於基本資料型別的轉換,以及非多型型別的轉換。例如,將int型別轉換為double型別,或將指標型別轉換為void*型別。

    如:

    int a = 10;
    double b = static_cast<double>(a);  // 將int型別的a轉換為double型別的b
    
    class Base {};
    class Derived : public Base {};
    Base* base = new Derived;
    Derived* derived = static_cast<Derived*>(base);  // 將基礎類別指標轉換為派生類指標
    
  2. dynamic_cast:用於多型型別的轉換,即具有虛擬函式的型別。它會在執行時檢查是否能夠安全地將一個指標或參照轉換為目標型別,如果不能則返回NULL。例如,將基礎類別指標轉換為派生類指標,或將派生類指標轉換為基礎類別指標。

    class Base {
    public:
        virtual void print() { cout << "Base" << endl; }
    };
    class Derived : public Base {
    public:
        void print() { cout << "Derived" << endl; }
    };
    
    Base* base = new Derived;
    Derived* derived = dynamic_cast<Derived*>(base);  // 將基礎類別指標轉換為派生類指標
    if (derived) {
        cout<<"derived"<<endl;  // 輸出"Derived"
    }else{
        cout<<"error deriving";
    }
    
  3. const_cast:用於去除const屬性。例如,將const int型別指標轉換為int型別指標,或將const物件轉換為非const物件。

    const int a = 10;
    int& b = const_cast<int&>(a);  // 將const int型別的a轉換為int型別的b的參照
    b = 20;
    cout << a << endl;  // 輸出10,因為a依然是const型別
    
  4. reinterpret_cast:用於進行二進位制的低層次轉換,不考慮型別之間的關係。例如,將一個指標轉換為一個整數,或將一個整數轉換為一個指標。

    int a = 10;
    int* p = &a;
    long long b = reinterpret_cast<long long>(p);  // 將int型別的指標p轉換為long long型別的b
    cout << b << endl;  // 輸出p的地址的十進位制表示
    

需要注意的是,這些型別轉換都具有一定的風險,需要謹慎使用。特別是reinterpret_cast,容易導致不可預測的錯誤,應該儘量避免使用。

memset

C++ 中的 memset 是一個用於填充記憶體塊的函數,其定義在標頭檔案 <cstring> 中。memset 可以將一段記憶體塊的值都設定為指定的值,常用於清空陣列或結構體等操作。

memset 函數的語法如下:

void* memset(void* ptr, int value, size_t num);

其中,第一個引數 ptr 是指向記憶體塊的指標,第二個引數 value 是要設定的值(通常是 0 或 -1),第三個引數 num 是要設定的位元組數。函數會將 ptr 指向的記憶體塊中的前 num 個位元組都設定為 value。

例如,下面的程式碼使用 memset 函數將一個陣列清空:

int arr[10];
memset(arr, 0, sizeof(arr));

在上面的程式碼中,我們將 arr 陣列中的所有元素都設定為 0。由於陣列中有 10 個元素,因此我們傳遞給 memset 函數的第三個引數是 sizeof(arr),表示要設定的位元組數。

需要注意的是,memset 函數並不會檢查陣列越界等錯誤,因此使用時需要確保不會存取到不屬於自己的記憶體區域。此外,對於非 POD 型別(即含有建構函式、解構函式或虛擬函式的型別),使用 memset 函數可能會導致不可預期的行為,因此需要謹慎使用。

assert

assert斷言,是C++<assert.h>庫的函數,用來找出程式的錯誤的。格式:assert(exp);

assert的第一個引數是一個表示式,就是用來找錯的表示式,如果為真則程式繼續執行,若為假則引起abort中斷訊號,程式終止執行。

如:

#include<assert.h>
#include<iostream>
int main(){
    int a=0;
    assert(a);

}

為什麼不用if

assert是用來排除錯誤的,而if是用來找異常的,錯誤是可以通過修改去掉的,而異常是無法避免的。

為什麼不直接cout

因為在一些大專案中,可以不止一個輸出,所以如果找到錯誤,後續的程式便不需要繼續執行。如:

#include<assert.h>
#include<iostream>
int main(){
    int a=0;
    int b=0;
    int c=0;
    //...
    assert(a);

    std::cout<<a<<" ";
    std::cout<<b<<" ";
    std::cout<<c<<" ";
    //...
}
使用規則
  • 根據上一條,所以assert一般用於程式輸出的開始

  • 每個assert只檢查一個條件,不然找到錯誤不知道是哪個

  • 不能改變環境的表示式

    如:assert(a++);這樣會改變環境的表示式要用,只能用assert(a),assert(a<100)這樣對原環境無影響的表示式

  • 一般assert()語句下一行空著,用來標註斷言語句