1. 程式人生 > 程式設計 >搞定技術面試:簡述 C++11/14 新特性

搞定技術面試:簡述 C++11/14 新特性

Introduction

同樣是C++工程師,有的人寫的是 C with object,有的人寫的是 C++ 98,fashion一點兒的寫 C++ 11/14,還有些程式設計師使用譚++。

上文只是段子,很多同學對 C++ 的瞭解僅停留在課堂上的理解,而不關注 C++ 的最新發展;事實上,C++ 的新特性很多可以大幅提高開發效率、程式執行效率以及提高程式碼的安全性和穩定性等。

本文主要關於左值右值、auto 關鍵字、智慧指標、default、delete、override、final、{}初始化、using別名、基於範圍的 for 迴圈、lambda 表示式 等內容,篇幅較長,請大家耐心閱讀多做實驗。

左值右值

C++( 包括 C) 中所有的表示式和變數要麼是左值,要麼是右值。通俗的左值的定義就是非臨時物件,那些可以在多條語句中使用的物件。所有的變數都滿足這個定義,在多條程式碼中都可以使用,都是左值。右值是指臨時的物件,它們只在當前的語句中有效。IBM 右值引用與轉移語義

有一種甄別表示式是否左值的方法,是檢查能否獲得該表示式的地址;如果可以取得,基本上可以斷定是左值表示式;如果不能取得則通常是右值。

C++ 中所有值都屬於 左值、右值兩者之一;若細分的話,右值可以分為:純右值、將亡值。

右值引用型別

我們可以理解右值為臨時物件,像有些函式返回的物件是臨時物件,該句執行完畢就會釋放臨時物件空間,因此留下右值的引用在以前並沒有用。

C++11 是提出了右值引用,可以延長臨時物件的生存週期,其建立方法為type && vb = xx;對應的左值引用的生命符號為&

#include <iostream>
#include <string>
 
int main()
{
    std::string s1 = "Test";
//  std::string&& r1 = s1;           // 錯誤:不能繫結到左值
 
    const std::string& r2 = s1 + s1; // okay:到 const 的左值引用延長生存期
//  r2 += "Test";                    // 錯誤:不能通過到 const 的引用修改
std::string&& r3 = s1 + s1; // okay:右值引用延長生存期 r3 += "Test"; // okay:能通過到非 const 的引用修改 std::cout << r3 << '\n'; } 複製程式碼

更重要的是,當函式同時具有右值引用和左值引用的過載時,右值引用過載繫結到右值(包含純右值和亡值),而左值引用過載繫結到左值:

#include <iostream>
#include <utility>
 
void f(int& x) {
    std::cout << "lvalue reference overload f(" << x << ")\n";
}
 
void f(const int& x) {
    std::cout << "lvalue reference to const overload f(" << x << ")\n";
}
 
void f(int&& x) {
    std::cout << "rvalue reference overload f(" << x << ")\n";
}
 
int main() {
    int i = 1;
    const int ci = 2;
    f(i);  // 呼叫 f(int&)
    f(ci); // 呼叫 f(const int&)
    f(3);  // 呼叫 f(int&&)
           // 若不提供 f(int&&) 過載則會呼叫 f(const int&)
    f(std::move(i)); // 呼叫 f(int&&)
 
    // 右值引用變數在用於表示式時是左值
    int&& x = 1;
    f(x);            // calls f(int& x)
    f(std::move(x)); // calls f(int&& x)
}
複製程式碼

移動語義(Move Sementics)與精準傳遞(Perfect Forwarding)

移動語義是通過盜取將亡值的變數記憶體空間,首先確保該部分空間之後不會使用,然後將該空間佔為己有,看起來像是一個拷貝操作。 移動語義位於標頭檔案#include<algorithm>,函式名為std::move

#include <iostream>
#include <vector>
#include <list>
#include <iterator>
#include <thread>
#include <chrono>
 
void f(int n)
{
    std::this_thread::sleep_for(std::chrono::seconds(n));
    std::cout << "thread " << n << " ended" << '\n';
}
 
int main() 
{
    std::vector<std::thread> v;
    v.emplace_back(f,1);
    v.emplace_back(f,2);
    v.emplace_back(f,3);
    std::list<std::thread> l;
    // copy() 無法編譯,因為 std::thread 不可複製
 
    std::move(v.begin(),v.end(),std::back_inserter(l)); 
    for (auto& t : l) t.join();
}
複製程式碼

自動型別推導

autodecltype 關鍵詞是新增的關鍵詞,我們知道C++是強型別語言,但使用這兩個關鍵詞,可以不用手寫完整型別,而是讓編譯器自行推導真實型別。

auto用法非常簡單,示例如下:

#include <iostream>
#include <utility>
 
template<class T,class U>
auto add(T t,U u) { return t + u; } // 返回型別是 operator+(T,U) 的型別
 
// 在其所呼叫的函式返回引用的情況下
// 函式呼叫的完美轉發必須用 decltype(auto)
template<class F,class... Args>
decltype(auto) PerfectForward(F fun,Args&&... args) 
{ 
    return fun(std::forward<Args>(args)...); 
}
 
template<auto n> // C++17 auto 形參宣告
auto f() -> std::pair<decltype(n),decltype(n)> // auto 不能從花括號初始化器列表推導
{
    return {n,n};
}
 
int main()
{
    auto a = 1 + 2;            // a 的型別是 int
    auto b = add(1,1.2);      // b 的型別是 double
    static_assert(std::is_same_v<decltype(a),int>);
    static_assert(std::is_same_v<decltype(b),double>);
 
    auto c0 = a;             // c0 的型別是 int,保有 a 的副本
    decltype(auto) c1 = a;   // c1 的型別是 int,保有 a 的副本
    decltype(auto) c2 = (a); // c2 的型別是 int&,為 a 的別名
    std::cout << "a,before modification through c2 = " << a << '\n';
    ++c2;
    std::cout << "a,after modification through c2 = " << a << '\n';
 
    auto [v,w] = f<0>(); // 結構化繫結宣告
 
    auto d = {1,2}; // OK:d 的型別是 std::initializer_list<int>
    auto n = {5};    // OK:n 的型別是 std::initializer_list<int>
//  auto e{1,2};    // C++17 起錯誤,之前為 std::initializer_list<int>
    auto m{5};       // OK:C++17 起 m 的型別為 int,之前為 initializer_list<int>
//  decltype(auto) z = { 1,2 } // 錯誤:{1,2} 不是表示式
 
    // auto 常用於無名型別,例如 lambda 表示式的型別
    auto lambda = [](int x) { return x + 3; };
 
//  auto int x; // 於 C++98 合法,C++11 起錯誤
//  auto x;     // 於 C 合法,於 C++ 錯誤
}
複製程式碼

decltype 的用處則是檢查實體的宣告型別,或者表示式的型別和值的型別。用法如下:decltype(實體/表示式)。可以使用另一個的實體的型別來定義新的變數。

#include <iostream>
 
struct A { double x; };
const A* a;
 
decltype(a->x) y;       // y 的型別是 double(其宣告型別)
decltype((a->x)) z = y; // z 的型別是 const double&(左值表示式)
 
template<typename T,typename U>
auto add(T t,U u) -> decltype(t + u) // 返回型別依賴於模板形參
{                                     // C++14 開始可以推導返回型別
    return t+u;
}
 
int main() 
{
    int i = 33;
    decltype(i) j = i * 2;
 
    std::cout << "i = " << i << ","
              << "j = " << j << '\n';
 
    auto f = [](int a,int b) -> int
    {
        return a * b;
    };
 
    decltype(f) g = f; // lambda 的型別是獨有且無名的
    i = f(2,2);
    j = g(3,3);
 
    std::cout << "i = " << i << ","
              << "j = " << j << '\n';
}
複製程式碼

函式返回值後置

可以通過這種寫法,將函式的返回值申明放在函式宣告的最後;auto function_name( 形參 ) (屬性,如 override等) (異常說明,可選) -> 返回值型別。 老實說,這種寫法讓我覺得自己寫的不是C++,估計大部分情況我不回去使用這個特性吧。。。

// 返回指向 f0 的指標的函式
auto fp11() -> void(*)(const std::string&)
{
    return f0;
}
複製程式碼

強制型別轉換

C++11起已經不建議使用C語言樣式的強制型別轉換,推薦使用static_cast、const_cast、reinterpret_cast、dynamic_cast等方法的型別轉換。

關鍵詞 說明
static_cast (常用) 用於良性轉換,一般不會導致意外發生,風險很低。
const_cast 用於 const 與非 const、volatile 與非 volatile 之間的轉換。
reinterpret_cast 高度危險的轉換,這種轉換僅僅是對二進位制位的重新解釋,不會藉助已有的轉換規則對資料進行調整,但是可以實現最靈活的 C++ 型別轉換。
dynamic_cast 藉助 RTTI,用於型別安全的向下轉型(Downcasting)。

C++四種型別轉換運運算元:static_cast、dynamic_cast、const_cast和reinterpret_cast

智慧指標

參見C++ 智慧指標

一些新的關鍵詞用法

nullptr

據說通常C++標頭檔案中NULL都是定義為#define NULL 0,因此本質上NULL的型別是int,使用NULL來表示空指標是非常不合適的行為,於是C++11重新定義了一個不是int型別且適用於空指標的關鍵詞。

關鍵詞 nullptr 代表指標字面量。它是 std::nullptr_t 型別的純右值。存在從 nullptr 到任何指標型別及任何成員指標型別的隱式轉換。同樣的轉換對於任何空指標常量也存在,空指標常量包括 std::nullptr_t 的值,以及巨集 NULL。nullptr,指標字面量

特殊成員函式

  1. 預設建構函式
  2. 複製建構函式
  3. 移動建構函式 (C++11 起)
  4. 複製賦值運運算元
  5. 移動賦值運運算元 (C++11 起)
  6. 解構函式

default

我們知道default本身是switch語句的關鍵詞,C++11中又擴充套件了新的用法,可以用來告訴編譯器生成預設的成員函式(預設建構函式等)。 特殊成員函式以及比較運運算元 (C++20 起)是僅有能被預置的函式,即使用 = default 替代函式體進行定義(細節見其相應頁面)

例如:預設建構函式可以使用 類名 ( ) = default ; (C++11 起) 方式宣告,然後可以不用在 *.cpp檔案中寫函式體實現,這個函式會使用編譯器預設生成。

delete 棄置函式

delete的新用法--棄置函式,相比於讓物件中的建構函式為私有,現在有了刪除該函式的方法。

如果取代函式體而使用特殊語法 = delete ;,則該函式被定義為棄置的(deleted)。任何棄置函式的使用都是非良構的(程式無法編譯)。這包含呼叫,包括顯式(以函式呼叫運運算元)及隱式(對棄置的過載運運算元、特殊成員函式、分配函式等的呼叫),構成指向棄置函式的指標或成員指標,甚或是在不求值表示式中使用棄置函式。但是,允許隱式 ODR 式使用 剛好被棄置的非純虛成員函式。

struct sometype
{
    void* operator new(std::size_t) = delete;
    void* operator new[](std::size_t) = delete;
};
sometype* p = new sometype; // 錯誤:嘗試呼叫棄置的 sometype::operator new
複製程式碼

override

這個關鍵詞翻譯為改寫,當指定一個虛擬函式覆蓋另一個虛擬函式時使用,Effective Modern C++一書中建議在該情況時一定加上該關鍵詞,這樣可以讓編譯器幫助我們檢查我們是否正確定義了覆蓋的函式(如果不正確定義則會編譯報錯)。

這部分程式碼將不會正確編譯,因為加了 override 後,編譯器會為我們尋找繼承的基類中對應的虛擬函式,而這裡就可以發現我們函式宣告上的一些錯誤。而如果不加override,這裡會成功編譯,但絕對不是我們想要的編譯結果。

/*
 * Key idea:
 *
 *   The below code won't compile,but,when written this way,compilers will
 *   kvetch about all the overriding-related problems.
 */

class Base {
public:
  virtual void mf1() const;
  virtual void mf2(int x);
  virtual void mf3() &;
  void mf4() const;
};

// Uncomment this,compile and see the compiler errors.
//class Derived: public Base {
//public:
//  virtual void mf1() override;
//  virtual void mf2(unsigned int x) override;
//  virtual void mf3() && override;
//  void mf4() const override;
//};
複製程式碼

可以只有override修飾的函式宣告正確才能夠成功編譯。

/*
 * Key idea:
 *
 *   This the code-example that uses override and is correct.
 */

class Base {
public:
  virtual void mf1() const;
  virtual void mf2(int x);
  virtual void mf3() &;
  virtual void mf4() const;
};

class Derived: public Base {
public:
  virtual void mf1() const override;
  virtual void mf2(int x) override;
  virtual void mf3() & override;
  void mf4() const override;         // adding "virtual" is OK,
};                                   // but not necessary
複製程式碼

final

宣告某一個虛擬函式不得被覆蓋。

( )、{ }初始化

有更多的方法初始化一個物件,比如花括號初始化列表例項如下:

/*
 * Key idea:
 *
 *   The treatment of braced initializers is the only way in which auto type
 *   deduction and template type deduction differ.
 */

#include <initializer_list>

template<typename T>  // template with parameter
void f(T param) {}    // declaration equivalent to
                      // x's declaration

template<typename T>
void f2(std::initializer_list<T> initList) {}

int main()
{
  {
    int x1 = 27;
    int x2(27);
    int x3 = {27};
    int x4{27};
  }

  {
    auto x1 = 27;    // type is int,value is 27
    auto x2(27);     // ditto
    auto x3 = {27};  // type is std::initializer_list<int>,
                     // value is {27}
    auto x4{27};     // ditto

    //auto x5 = {1,2,3.0};  // error! can't deduce T for
    //                        // std::initializer_list<T>
  }

  {
    auto x = { 11,23,9 };  // x's type is
                             // std::initializer_list<int>

    //f({ 11,23,9 });        // error! can't deduce type for T

    f2({ 11,9 });        // T deduced as int,and initList's
                              // type is std::initializer_list<int>
  }
}
複製程式碼

using 別名

除了 typedef關鍵詞,還可以使用using關鍵詞建立別名,Effective Modern C++一書更推薦使用別名宣告。

/*
 * Key Idea:
 *
 *   Using alias declarations is easier to read than function pointers.
 */

#include <string>

// FP is a synonym for a pointer to a function taking an int and
// a const std::string& and returning nothing
typedef void (*FP)(int,const std::string&);    // typedef

// same meaning as above
using FP = void (*)(int,const std::string&);   // alias
                                                // declaration

複製程式碼

限定作用域的 列舉型別

通過在列舉型別定義中加一個關鍵詞,可以限制列舉型別的作用域。enum test-> enum class test

/*
 * Key Idea:
 *
 *   In C++11,the names of scoped enums do not belong to the scope containing
 *   the enum.
 */

enum class Color { black,white,red };  // black,red
                                         // are scoped to Color

auto white = false;              // fine,no other
                                 // "white" in scope

//Color c1 = white;                 // error! no enumerator named
                                 // "white" is in this scope

Color c2 = Color::white;          // fine

auto c3 = Color::white;           // also fine (and in accord
                                 // with Item4's advice)
複製程式碼

基於範圍的for迴圈

C++也可以像python語言那樣使用基於範圍的for迴圈了,是一個進步吧,集各家之所長。 基於範圍的for迴圈語法是for(範圍宣告:範圍表示式)。其中,範圍宣告:一個具名變數的宣告,其型別是由 範圍表示式 所表示的序列的元素的型別,或該型別的引用,通常用 auto 說明符進行自動型別推導;範圍表示式:任何可以表示一個合適的序列(陣列,或定義了 begin 和 end 成員函式或自由函式的物件,見下文)的表示式,或一個花括號初始化器列表,基本上std中幾個常見容器,如:vector、list等都是支援基於範圍的for迴圈的。

#include <iostream>
#include <vector>
 
int main() {
    std::vector<int> v = {0,1,2,3,4,5};
 
    for (const int& i : v) // 以 const 引用訪問
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (auto i : v) // 以值訪問,i 的型別是 int
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (auto& i : v) // 以引用訪問,i 的型別是 int&
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (int n : {0,5}) // 初始化器可以是花括號初始化器列表
        std::cout << n << ' ';
    std::cout << '\n';
 
    int a[] = {0,5};
    for (int n : a) // 初始化器可以是陣列
        std::cout << n << ' ';
    std::cout << '\n';
 
    for (int n : a)  
        std::cout << 1 << ' '; // 不必使用迴圈變數
    std::cout << '\n';
 
}
複製程式碼

lambda 表示式

lambda 表示式即是無名函式,很像java中的臨時函式(集各家之所長,比各家難用……) lambda的語法如下:

[ 俘獲 ] <模板形參>(可選)(C++20) ( 形參 ) 說明符(可選) 異常說明 attr -> ret requires(可選)(C++20) { 函式體 }	
[ 俘獲 ] ( 形參 ) -> ret { 函式體 }	
[ 俘獲 ] ( 形參 ) { 函式體 }	
[ 俘獲 ] { 函式體 }	
複製程式碼

lambda 表示式細節更多,有可能單獨寫一個部落格進行解釋說明,如果大家有興趣的話,可以先看看zh.cppreference.com這篇說明。

Reference

  1. Effective Modern C++
  2. C++11/14高階程式設計
  3. IBM 右值引用與轉移語義
  4. cppreference.com 引用宣告
  5. cppreference.com auto
  6. C++四種型別轉換運運算元:static_cast、dynamic_cast、const_cast和reinterpret_cast
  7. cppreference.com nullptr,指標字面量
  8. cppreference.com 特殊成員函式
  9. cppreference.com 棄置函式
  10. 花括號初始化列表
  11. 基於範圍的for迴圈
  12. lambda 表示式