主頁 > 後端開發 > 徹底理解c++的隱式型別轉換

徹底理解c++的隱式型別轉換

2021-02-20 06:06:27 後端開發

隱式型別轉換可以說是我們的老朋友了,在代碼里我們或多或少都會依賴c++的隱式型別轉換,

然而不幸的是隱式型別轉換也是c++的一大坑點,稍不注意很容易寫出各種奇妙的bug,

因此我想借著本文來梳理一遍c++的隱式型別轉換,復習的同時也避免其他人踩到類似的坑,

本文索引

  • 什么是隱式型別轉換
  • 基礎回顧
    • 直接初始化
    • 復制初始化
    • 型別構造時的隱式轉換
  • 隱式轉換是如何作業的
    • 標準轉換
    • 用戶自定義轉換
    • 隱式轉換序列
  • 隱式轉換引發的問題
    • 參考系結
    • 陣列退化
    • 兩步轉換
  • 總結
    • 參考資料

什么是隱式型別轉換

借用標準里的話來說,就是當你只有一個型別T1,但是當前運算式需要型別為T2的值,如果這時候T1自動轉換為了T2那么這就是隱式型別轉換,

如果你覺得太抽象的話可以看兩個例子,首先是最常見的混用數值型別:

int a = 0;
long b = a + 1; // int 轉換為 long

if (a == b) {
    // 默認的operator==需要a的型別和b相同,因此也發生轉換
}

int轉成long是向上轉換,通常不會有太大問題,而long到int則很可能導致資料丟失,因此要盡量避免后者,

第二個例子是自定義型別到標量型別的轉換:

std::shared_ptr<int> ptr = func();
if (ptr) { // 這里會從shared_ptr轉換成bool
    // 處理資料
}

因為提供了用戶自定義的隱式型別轉換規則,所以我們可以很簡單地去判斷智能指標是否為空,在這里if運算式里需要bool,因此ptr轉換為了bool,這又被叫做語境轉換,

理解了什么是隱式型別轉換轉換之后我們再來看看那些不允許進行隱式轉換的語言,比如golang:

var a int32 = 0;
var b int64 = 1;

fmt.Println(a + b) // error!
fmt.Println(int64(a) + b)

編譯器會告訴你型別不同無法運算,一個更災難性的例子如下:

sleepDuration := 2.5
time.Sleep( time.Duration(float64(time.Millisecond) * ratio) ) // 休眠2.5ms

本身是非常簡單的代碼,然而多層嵌套式的型別轉換帶來了雜音,代碼可讀性嚴重下降,

這種形式的型別轉換被稱為顯式型別轉換,在c++里是這樣的:

A a{1};
B b = static_cast<B>(a);

static_cast被用于將某個型別轉換到其相關的型別,需要用戶指明待轉換到的型別,除此之外還有const_cast等cast,它們負責了c++中的顯式型別轉換,

由此可見隱式型別轉換轉換可以簡化代碼的書寫,不過簡化不是沒有代價的,我們細細說來,

基礎回顧

在正式介紹隱式型別轉換之前,我們先要回顧一下基礎知識,放輕松,

直接初始化

首先是類的直接初始化,

顧名思義,就是顯式呼叫型別的建構式進行初始化,舉個例子:

struct A {
    A() = default;
    A(const A&) = default;
    A(int) {}
};

// 這是默認初始化: A a; 注意區分

A a1{}; // c++11的串列初始化
// 不能寫出A a2(),因為這會被認為是函式宣告
A a2(1);
A a3(a2); // 沒錯,顯式呼叫復制建構式也是直接初始化

auto a4 = static_cast<A>(1);

需要注意的是a4,用static_cast轉換成型別T的這一步也是直接初始化,

這種初始化方式有什么用呢?直接初始化會考慮全部的建構式,而不會忽略explicit修飾的建構式,

顯式地呼叫建構式進行直接初始化實際上是顯式型別轉換的一種,

復制初始化

除去默認初始化和直接初始化,剩下的會導致復制的基本都是復制初始化,典型的如下:

A func() {
    return A{}; // 回傳值會被復制初始化
}

A a5 = 1; // 先隱式轉換,再復制初始化

void func2(A a) {} // 非參考的引數傳遞也會進行復制構造

然而類似A a6 = {1}的運算式卻不是復制初始化,這是復制串列初始化,會直接選擇合適的非explicit建構式進行初始化,而不用創建臨時量再進行復制,

復制初始化又起到什么作用呢?

首先想到的是這樣可以創造某個物件的副本,沒錯,不過還有一個更重要的作用:

如果想要某個型別T1的value能進行到T2的隱式轉換,兩個型別必須滿足這個運算式的呼叫T2 v2 = value

而這個形式的運算式正是復制初始化運算式,至于具體的原因,我們馬上就會在下一節看到,

型別構造時的隱式轉換

在進入本節前我們看一道經典的面試題:

std::string s = "hello c++";

請問創建了幾個string呢?如果你脫口而出1個,那么面試官八成會狡黠一笑,讓你回家等通知去了,

那么答案是什么呢?是1個或者2個,什么,你逗我呢?

先別急,我們分情況討論,首先是c++11之前,

在c++11前題目里的運算式實際上會導致下面的行為:

  1. 首先"hello c++"const char[N]型別的,不過它在運算式中于是退化成const char *
  2. 然后因為s實際上是處于“宣告即定義”的運算式中,因此適用的只有復制建構式,而不是多載的=
  3. 因此等號的右半邊必須也是string型別
  4. 因為正好有從const char *string的轉換規則,因此把它轉換成合適的型別
  5. 轉換完會回傳一個新的string的臨時量,它會作為引數呼叫復制建構式
  6. 復制建構式呼叫完成后s也就創建完畢了,

在這里我們暫且忽略了string的寫時復制等黑科技,整個程序創建了s和一個臨時量,一共兩個string,

很快c++11就出現了,同時還帶來了移動語意,然而結果并沒有改變:

  1. 前面步驟相同,字串字面量隱式轉換成string,創建了一個臨時量
  2. 臨時量是個右值,所以系結給右值參考,因此移動建構式被選擇
  3. 臨時量里的資料移動到s里,s創建完成

移動語意減少了不必要的內部資料的復制,但是臨時量還是會被創建的,

有進搗鼓編譯器的朋友可能要說了,編譯器是不生成這個臨時量的,是這樣的,編譯器會用復制省略(copy elision)優化這段代碼,

是的,復制省略在c++11里就已經被提到了,不過那時候它是可選的,并不強制編譯器支持這一優化,因此你在GCC和clang上觀察到的不一定能代表全部的c++編譯器的情況,所以我們仍以標準為基礎推演了理論上的行為,

到目前為止答案都是2,然而很快有意思的事情發生了——復制省略在c++17里成為了被標準化的行為,

在c++17里除非必要,否則臨時量(現在叫做右值的結果物件,一個右值只有在實際需要存在一個臨時變數的情況下才會創建一個臨時變數,這個程序叫做實質化,創建出來的那個臨時量就是該右值的結果物件)不會被創建,換而言之,T obj = expr這樣的形式會以expr產生結果直接呼叫合適的建構式,而不會進行臨時量的創建和復制建構式的呼叫,不過為了保證語意的完整性,復制建構式仍然被要求是可訪問的,畢竟類本身不允許復制構造的話復制初始化本身就是不正確的,不能因為復制省略而導致錯誤的代碼被編譯通過,

所以現在程序變成了下面這樣子:

  1. 編譯器發現運算式是string的復制初始化
  2. 右側是運算式會隱式轉換產生一個string的純右值用于初始化同一型別的s
  3. 判斷復制建構式是否可用,然后發現符合復制省略的條件
  4. 尋找string里是否有符合要求的建構式
  5. 找到了string::string(const char *),于是直接呼叫
  6. s初始化完成

因此,在c++17下只會創建一個string物件,這比移動語意更加高效,這也是為什么我說題目的答案既可以是1也可以是2的原因,

同時我們還發現,在復制構造時的型別轉換不管復制有沒有被省略都是存在的,只不過換了一個形式,這就是我們后面要講的內容,

隱式轉換是如何作業的

復習完基礎知識,我們可以進入正題了,

隱式轉換可以分為兩個部分,標準定義的轉換和用戶自定義的轉換,我們先來看看它們是什么,

標準轉換

也就是編譯器里內置的一些型別轉換規則,比如陣列退化成指標,函式轉換成函式指標,特定語境下要求的轉換(if里要求bool型別的值),整數型別提升,數值轉換,資料型別指標到void指標的轉換,nullptr_t到資料型別指標的轉換等,

底層const和volatie也可以被轉換,只不過只能添加不能減少,可以把T*轉換成const T*,但反過來是不可以的,

這些轉換基本都是針對標量型別和陣列這種內置的聚合型別的,

如果想要指定自定義型別的轉換規則,則需要撰寫用戶自定義型別轉換的介面了,

用戶自定義轉換

說了這么多,也該看看用戶自定義轉換了,

用戶能控制的自定義轉換介面一共也就兩個,轉換建構式和用戶定義轉換函式,

轉換建構式就是只類似T(T2)這樣的建構式,它擁有一個顯式的T2型別的引數,通過這個建構式可以實作從T2轉換型別至T1的效果,

用戶定義轉換函式是類似operator T2()這樣的類方法,注意不需要指定回傳值,通過它可以實作從T1轉換到T2,可轉換的型別包括自身T1(還可附加cv限定符,或者參考)、T1的基類(或參考)以及void,

舉個例子:

struct A {};

struct B {
    // 轉換建構式
    B(int);
    B(const A&);

    // 用戶定義轉換函式,不需要顯式指定回傳值
    operator A();
    operator int();
}

上面的B自定義了轉換規則,既可以從int和A轉換成B,也可以從B轉換成int和A,

不難看出規則是這樣的:

T  <---轉換建構式---  其他型別

T  ---用戶定義轉換函式--->  其他型別

這里的轉換建構式是指沒有explicit限定的,有的話就不能用于隱式型別轉換,

從c++11開始explicit還可以用于用戶定義的轉換函式,例如:

template <typename T>
struct SmartPointer {
    //...
    T *ptr = nullptr;
    // 方便判斷指標是否為空
    explicit operator bool() {
        return ptr != nullptr;
    }
};

SmartPointer<int> p = func();
if (p) {
    p << 1; // 這是不允許的
}

這樣的型別轉換函式只能用于顯式初始化以及特定語境要求的型別轉換(比如if里的條件運算式要求回傳bool值,這算隱式轉換的一種),因此可以避免注釋標注的那種語意錯誤,因此這類轉換函式也無法用于其他的隱式轉換,

c++11開始函式可以自動推導回傳值,模板和自動推到也可以用于自定義的轉換函式:

template <typename T>
struct SmartPointer {
    //...
    T *ptr = nullptr;
    explicit operator bool() {
        return ptr != nullptr;
    }

    // 配合模板引數
    operator T*() {
        return ptr;
    }

    /* 自動推到回傳值,與上一個同義
    operator auto() {
        return ptr;
    }
    */
};

SmartPointer<int> p = func();
int *p1 = p;

最后用戶自定義的轉換函式還可以是虛函式,但是只有從基類的參考或指標進行派發的時候才會呼叫子類實作的轉換函式:

struct D;
struct B {
    virtual operator D() = 0;
};
struct D : B
{
    operator D() override { return D(); }
};
 
int main()
{
    D obj;
    D obj2 = obj; // 不呼叫 D::operator D()
    B& br = obj;
    D obj3 = br; // 通過虛派發呼叫 D::operator D() 
}

用戶定義轉換函式不能是類的靜態成員函式,

隱式轉換序列

了解完標準內置的轉換規則和用戶自定義的轉換規則,我們該看看隱式轉換的作業機制了,

對于需要進行隱式轉換的背景關系,編譯器會生成一個隱式轉換序列:

  1. 零個或一個由標準轉換規則組成的標準轉換序列,叫做初始標準轉換序列
  2. 零個或一個由用戶自定義的轉換規則構成的用戶定義轉換序列
  3. 零個或一個由標準轉換規則組成的標準轉換序列,叫做第二標準轉換序列

對于隱式轉換發生在建構式的引數上時,第二標準轉換序列不存在,

初始標準轉換序列很好理解,在呼叫用戶自定義轉換前先把值的型別處理好,比如加上cv限定符:

struct A {};
struct B {
    operator A() const;
};

const B b;
const A &a = b;

初始標準轉換序列會把值先轉換成適當的形式以供用戶轉換序列使用,在這里operator A() const希望傳進來的this是const B*型別的,而對b直接取地址只能得到B*,正好標準轉換規則里有添加底層const的規則,所以適用,

如果值的型別正好,不需要任何預處理,那么初始標準轉換序列不會做任何多余的操作,

如果第一步還不能轉換出合適的型別,那么就會進入用戶定義轉換序列,

如果型別是直接初始化,那么只會呼叫轉換建構式;如果是復制初始化或者參考系結,那么轉換建構式和用戶定義轉換函式會根據多載決議確定使用誰,另外如果轉換函式不是const限定的,那么在兩者都是可行函式時優先選擇轉換函式,比如operator A();這樣的,否則會報錯有歧義(GCC 10.2上測驗顯示有歧義的時候會選擇轉換建構式,clang++11.0和標準描述一致),這也是我們復習了幾種初始化有什么區別的原因,因為類的構造形式不同結果也可能會不同,

選擇好一個規則后就可以進入下一步了,

如果是在建構式的引數上,那么隱式轉換到此就結束了,除此之外我們需要進行第三部,

第三部是針對用戶轉換序列處理后的值的型別做一些善后作業,之所以不允許在建構式的引數上執行這一步是因為防止過度轉換后和用戶轉換規則產生回圈,

舉個例子:

struct A
{
    operator int() const;
};

A a;
bool b = a;

在這里a只能轉換成int,而為了偷懶我們直接把a隱式轉換成bool,問題來了,初始標準轉換序列把A*轉換成了const A*(作為this,類方法的隱式引數),用戶轉換序列把const A*轉換為了int,int和bool是完全不同的型別,怎么辦呢?

這就用上第二標準轉換序列了,這里是數值轉換,int轉成bool,

不過上面只是個例子,請不要這么寫,因為在實際代碼中會出現問題:

template <typename T>
struct SmartPointer {
    //...
    T *ptr = nullptr;
    operator bool() {
        return ptr != nullptr;
    }

    T& operator*() {
        return *ptr;
    }
};

auto ptr = get_smart_pointer();
if (ptr) {
    // ptr 是int*的包裝,現在我們想取得ptr指向的值
    int value = https://www.cnblogs.com/apocelipes/p/p;
    // ...
}

上面的代碼不會有任何編譯錯誤,然而它將引發嚴重的運行時錯誤,

為什么呢?因為如注釋所說我們想取得指標指向的值,然而我們忘記解參考了!實際上因為要轉換成int,隱式轉換序列里是這樣的:

  1. 初始標準轉換序列 -----> 當前型別已經呼叫用戶轉換序列的要求了,什么都不做
  2. 用戶定義轉換序列 -----> 和int最接近的有轉換關系的型別只有bool了,呼叫這個
  3. 第二標準轉換序列 -----> 得到了bool,目標的int,正好有規則可用,進行轉換

因此你的value只會有兩種值,0和1,這就是隱式轉換帶來的第一個大坑,而上面代碼反應出的問題叫做“安全bool(safe bool)”問題,

好在我們可以用explicit把它踢出轉換序列:

template <typename T>
struct SmartPointer {
    //...
    T *ptr = nullptr;
    explicit operator bool() {
        return ptr != nullptr;
    }
};

這樣當再寫出int value = https://www.cnblogs.com/apocelipes/p/p的時候編譯器就能及時發現并報錯啦,

第二標準轉換序列的本意是幫我們善后,畢竟類的撰寫者很難面面俱到,然而也正是如此帶來了一些坑點,

還有另外一點要注意,標準規定了如果用戶轉換序列轉換出了一個左值(比如一個左值參考),而最終轉換目標的右值參考,那么標準轉換中的左值轉換為右值的規則不可用,程式是無法通過編譯的,比如:

struct A
{
    operator int&();
};

int&& b = A();

編譯上面的代碼,g++會獎勵你一句cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’

如果隱式轉換序列里一個可行的轉換都沒有呢?那很遺憾,只能編譯報錯了,

隱式轉換引發的問題

現在我們已經知道隱式轉換的作業方式了,而且我們也看到了隱式型別轉換是如何闖禍的,

下面將要介紹隱式型別轉換闖了禍怎么善后,以及怎么防患于未然,

是時候和實際應用碰撞出點火花了,

參考系結

第一個問題是和參考相關的,不過與其說是隱式轉換惹的禍倒不如說是參考系結自身的坑,

我們知道對于一個型別T,可以有這幾種參考型別:

  • T&,T的參考,只能系結到T型別的左值
  • const T&,const T的參考,可以系結到T的左值和右值,以及const T的左值和右值
  • T&&,T的右值參考,只能系結到T型別的右值
  • const T&&,一般來說見不到,然而當你對一個const T&使用std::move就能得到這東西了

參考必須在宣告的同時進行初始化,所以下面這樣的代碼應該是大家再熟悉不過的了:

int num = 0;
const int &a = num;
int &b = num;
int &&c = 100;
const int &d = 100;

新的問題出現了,考慮一下如下代碼的運行結果:

int a = 10;
long &b = a;
std::cout << b << std::endl;

不是10嗎?還真不是:

c.cpp: In function ‘int main()’:
c.cpp:6:11: error: cannot bind non-const lvalue reference of type ‘long int&’ to an rvalue of type ‘long int’
    6 | long &b = a;
      | 

報錯說得很清楚了,一個普通的左值參考不能系結到一個右值上,因為a是int,b是long,所以a想賦值給b就必須先隱式轉換成long,

隱式轉換除非是轉成參考型別,否則一般都是右值,所以這里報錯了,解決辦法也很簡單:

long b1 = a;
const long &b2 = a;

要么直接復制構造一個新的long型別變數,值型別的變數可以從右值初始化;要么使用const左值參考,因為它能系結到右值,

擴展一下,函式的引數傳遞也是如此:

void func(unsigned int &)
{
    std::cout << "lvalue reference" << std::endl;
}

void func(const unsigned int &)
{
    std::cout << "const lvalue reference" << std::endl;
}

int main()
{
    int a = 1;
    func(a);
}

結果是“const lvalue reference”,這也是為什么很多教程會叫你盡量多使用const lvalue參考的原因,因為除了本身的型別T,這樣的函式還可以通過隱式型別轉換接受其他能轉換成T的資料做引數,并且相比創建一個物件并復制初始化成引數,應用的開銷更小,當然右值最優先匹配的是右值參考,所以如果有形如void func(unsigned int &&)的多載存在,那么這個多載會被呼叫,

最典型的應用非下面的例子莫屬了:

template <typename... Args>
void format_and_print(const std::string &s, Args&&... args)
{
    // 實作格式化并列印出結果
}

std::string info = "%d + %d = %d\n";
format_and_print(info, 2, 2, 4);
format_and_print("%d * %d = %d\n", 2, 2, 4);

只要能隱式轉換成string,就能直接呼叫我們的函式,

最重要的一點,隱式型別轉換產生的通常是右值,(當然顯式型別轉換也一樣,不過在隱式轉換的時候更容易忘了這點)

陣列退化

同樣是隱式轉換帶來的經典問題:陣列在求值運算式中退化成指標,

你能給出下面代碼的輸出嗎:

void func(int arr[])
{
    std::cout << (sizeof arr) << std::endl;
}

int main()
{
    int a[100] = {0};
    std::cout << (sizeof a) << std::endl;
    func(a);
}

在我的amd64 Linux上使用GCC 10.2編譯運行的結果是400和8,后者其實是該系統上int*的大小,因為sizeof不求值而函式引數傳遞是求值的,所以陣列退化成了指標,

這樣的隱式轉換帶來的壞處是什么呢?答案是陣列的長度丟失了,假如你不知道這一點,在函式中仍然用sizeof去求陣列的大小,那么難免不會出問題,

解決辦法有很多,比如最簡單的借助模板:

template <std::size_t N>
void func(int (&arr)[N])
{
    std::cout << (sizeof arr) << std::endl; // 400
    std::cout << N << std::endl; // 100
}

現在N是100,而sizeof會回傳400,因為sizeof一個參考會回傳參考指向的型別的大小,這里是int [100]

一個更簡單也更為現代c++推崇的做法是放棄原始陣列,把它當做沉重的歷史包袱丟棄掉,轉而使用std::array和即將到來的std::span,這些更現代化的陣列替代品可以更好得代替原始陣列而不會發生諸如隱式轉換成指標等問題,

兩步轉換

還有不少教程會告訴你在隱式轉換的時候超過一次的型別轉換是不可以的,我習慣把這種問題叫做“兩步轉換”,

為什么叫兩步轉換呢?假如我們有ABC三個型別,A可以轉B,B可以轉C,他們是單步的轉換,而如果我們需要把A轉成C,就需要先把A轉成B,因為A不能直接轉換成C,因此形成了一個轉換鏈:A -> B -> C,其中進行了兩次型別轉換,我稱其為兩步轉換,

下面是一個典型的“兩步轉換”:

struct A{
    A(const std::string &s): _s{s} {}
    std::string _s;
};

void func(const A &s)
{
    std::cout << s._s << std::endl;
}

int main()
{
    func("two-steps-implicit-conversion");
}

我們知道const char*能隱式轉換到string,然后string又可以隱式轉換成A:const char* -> string -> A,而且函式引數是個常量左值參考,應該能系結到隱式轉換產生的右值,然而用g++編譯代碼會是下面的結果:

test.cpp: In function 'int main()':
test.cpp:15:10: error: invalid initialization of reference of type 'const A&' from expression of type 'const char [30]'
   15 |     func("two-steps-implicit-conversion");
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test.cpp:8:20: note: in passing argument 1 of 'void func(const A&)'
    8 | void func(const A &s)
      |           ~~~~~~~~~^

果然報錯了,可是這真的是因為兩步轉換帶來的結果嗎?我們稍微改一改代碼:

struct A{
    A(bool b)
    {
        _s = b ? "received true" : "received false";
    }
    std::string _s;
};

void func(const A &s)
{
    std::cout << s._s << std::endl;
}

int main()
{
    int num = 0;
    func(num); // received false
    unsigned long num2 = 100;
    func(num2); // received true
}

這次不僅編譯通過,而且指定-Wall -Wextra也不會有任何警告,輸出也是正常的,

那就怪了,這里的兩次呼叫分別是int -> bool -> Aunsigned long -> bool -> A,很明星的兩步轉換,怎么就是合法的正常代碼呢?

其實答案早在隱式轉換序列那節就告訴過你了:

一個隱式型別轉換序列包括一個初始標準轉換序列、一個用戶定義轉換序列、一個第二標準轉換序列

也就是說不存在什么兩步轉換問題,本身轉換序列最少可以轉換1次,最多可以三次,兩次轉換當然沒問題了,

唯一會觸發問題的是出現了兩次用戶定義轉換,因為隱式轉換序列里只允許一次用戶定義轉換,語言標準也規定了不允許出現多余一次的用戶定義轉換:

At most one user-defined conversion (constructor or conversion function) is implicitly applied to a single value. -- 12.3 Conversions [class.conv]

所以這條轉換鏈:const char* -> string -> A 是有問題的,因為從字串字面量到string和string到A都是用戶定義轉換,

int -> bool -> Aunsigned long -> bool -> A這兩條是沒問題的,第一次轉換是初始標準轉換序列完成的,第二次是用戶定義轉換,整個程序合情合理,

由此看來教程們只說對了一半,“兩步轉換”的癥結在于一次隱式轉換中不能出現兩次用戶定義的型別轉換,這個問題叫做“兩步自定義轉換”更恰當,

用戶定義的型別轉換只能出現在自定義型別中,這其中包括了標準庫,所以換句話說,當你有一條A -> B -> C這樣的隱式轉換鏈的時候,如果其中有兩個都是自定義型別,那么這個隱式轉換是錯誤的,

唯一的解決辦法就是把第一次發生的用戶自定義轉換改成顯式型別轉換:

struct A{
    A(const std::string &s): _s{s} {}
    std::string _s;
};

void func(const A &s)
{
    std::cout << s._s << std::endl;
}

int main()
{
    func(std::string{"two-steps-implicit-conversion"});
}

現在隱式轉換序列里只有一次自定義轉換了,問題也就不會發生了,

總結

相信現在你已經徹底理解c++的隱式型別轉換了,常見的坑應該也能繞過了,

但我還是得給你提個醒,盡量不要去依賴隱式型別轉換,多用explicit和各種顯式轉換,少想當然,

Keep It Simple and Stupid.

參考資料

https://zh.cppreference.com/w/cpp/language/copy_elision

http://www.cplusplus.com/doc/tutorial/typecasting/

https://en.cppreference.com/w/cpp/language/implicit_conversion

https://stackoverflow.com/questions/26954276/second-standard-conversion-sequence-of-user-defined-conversion

https://stackoverflow.com/questions/48576011/why-does-const-allow-implicit-conversion-of-references-in-arguments/48576055

https://zh.cppreference.com/w/cpp/language/cast_operator

https://www.nextptr.com/tutorial/ta1211389378/beware-of-using-stdmove-on-a-const-lvalue

https://en.cppreference.com/w/cpp/language/reference_initialization

https://stackoverflow.com/questions/12847272/multiple-implicit-conversions-on-custom-types-not-allowed

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/261225.html

標籤:C++

上一篇:idea2020.1配置了maven提示程式包不存在

下一篇:編譯器實作之旅——第一章 編譯器概觀

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more