【面經筆記】C++語法

C++11有哪些新特性?

auto類型推導

Override和final

lambda表達式

constexpr常量表達式

智能指針:weak_ptr、shared_ptr、unique_ptr

Move語義、右值引用


c++中的隱藏、重載、覆蓋(重寫)

成員函數被重載的特徵:

(1)相同的範圍(在同一個類中);

(2)函數名字相同;

(3)參數不同;

(4)virtual關鍵字可有可無。

覆蓋是指派生類函數覆蓋基類函數,特徵是:

(1)不同的範圍(分別位於派生類與基類);

(2)函數名字相同;

(3)參數相同;

(4)基類函數必須有virtual關鍵字。

令人迷惑的隱藏規則:

本來僅僅區別重載與覆蓋並不算困難,但是C++的隱藏規則使問題複雜性陡然增加。這裏“隱藏”是指派生類的函數屏蔽了與其同名的基類函數,規則如下:

(1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。

(2)如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)。

#include <iostream.h>

    class Base
{
public:
    virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
  virtual void g(float x){ cout << "Base::g(float) " << x << endl; }
  void h(float x){ cout << "Base::h(float) " << x << endl; }
}; 


 class Derived : public Base
{
public:
    virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
  virtual void g(int x){ cout << "Derived::g(int) " << x << endl; }
   void h(float x){ cout << "Derived::h(float) " << x << endl; }
}; 

上面的程序中:
(1)函數Derived::f(float)覆蓋了Base::f(float)。
(2)函數Derived::g(int)隱藏了Base::g(float),而不是重載。
(3)函數Derived::h(float)隱藏了Base::h(float),而不是覆蓋。


override:表示函數應當覆蓋/重寫基類中的虛函數。

final:表示派生類不應當覆蓋/重寫這個虛函數。


左值、右值

在C++11中所有的值必屬於左值、右值兩者之一,右值又可以細分爲純右值、將亡值。
在c++11中可以取地址的、有名字的就是左值,反之,不能取地址的、沒有名字的就是右值(將亡值或純右值)。舉個例子,int a = b+c, a 就是左值,其有變量名爲a,通過&a可以獲取該變量的地址;表達式b+c、函數int func()的返回值是右值,在其被賦值給某一變量前,我們不能通過變量名找到它,&(b+c)這樣的操作則不會通過編譯。

右值、將亡值

在理解C++11的右值前,先看看C++98中右值的概念:C++98中右值是純右值,純右值指的是臨時變量值、不跟對象關聯的字面量值。臨時變量指的是非引用返回的函數返回值、表達式等,例如函數int func()的返回值,表達式a+b;不跟對象關聯的字面量值,例如true,2,”C”等。

C++11對C++98中的右值進行了擴充。在C++11中右值又分爲純右值(prvalue,Pure Rvalue)和將亡值(xvalue,eXpiring Value)。其中純右值的概念等同於我們在C++98標準中右值的概念,指的是臨時變量和不跟對象關聯的字面量值;將亡值則是C++11新增的跟右值引用相關的表達式,這樣表達式通常是將要被移動的對象(移爲他用),比如返回右值引用T&&的函數返回值、std::move的返回值,或者轉換爲T&&的類型轉換函數的返回值。

將亡值可以理解爲通過“盜取”其他變量內存空間的方式獲取到的值。在確保其他變量不再被使用、或即將被銷燬時,通過“盜取”的方式可以避免內存空間的釋放和分配,能夠延長變量值的生命期。

左值引用、右值引用

左值引用就是對一個左值進行引用的類型。右值引用就是對一個右值進行引用的類型,事實上,由於右值通常不具有名字,我們也只能通過引用的方式找到它的存在。

右值引用和左值引用都是屬於引用類型。無論是聲明一個左值引用還是右值引用,都必須立即進行初始化。而其原因可以理解爲是引用類型本身自己並不擁有所綁定對象的內存,只是該對象的一個別名。左值引用是具名變量值的別名,而右值引用則是不具名(匿名)變量的別名

左值引用不能綁定到右值對象上,右值引用也不能綁定到左值對象上。
例外:如果左值引用是const類型的,則其可以綁定到右值對象上。

左值引用通常也不能綁定到右值,但常量左值引用是個“萬能”的引用類型。它可以接受非常量左值、常量左值、右值對其進行初始化。不過常量左值所引用的右值在它的“餘生”中只能是隻讀的。相對地,非常量左值只能接受非常量左值對其進行初始化

int &a = 2;       # 左值引用綁定到右值,編譯失敗

int b = 2;        # 非常量左值
const int &c = b; # 常量左值引用綁定到非常量左值,編譯通過
const int d = 2;  # 常量左值
const int &e = c; # 常量左值引用綁定到常量左值,編譯通過
const int &b =2;  # 常量左值引用綁定到右值,編程通過

右值值引用通常不能綁定到任何的左值,要想綁定一個左值到右值引用,需要std::move()將左值強制轉換爲右值,例如:

int a;
int &&r1 = c;             # 編譯失敗
int &&r2 = std::move(a);  # 編譯通過

由於右值引用只能綁定到右值對象上,而右值對象又是短暫的、即將銷燬的。也就是說右值引用有一個重要性質:只能綁定到即將銷燬的對象上。
只要能夠綁定右值的引用類型,都能夠延長右值的生命期。

參考1
參考2


常量表達式

一. constexpr和常量表達式

  常量表達式(const expression)是指值不會改變並且在編譯過程就能得到計算結果的表達式。顯然,字面值屬於常量表達式,用常量表達式初始化的const對象也是常量表達式。

  一個對象(或表達式)是不是常量表達式由它的數據類型和初始值共同決定,例如:

const int max_files = 20;       // max_files是常量表達式  
const int limit = max_files + 1;    // limit是常量表達式  
int staff_size = 27;                // staff_size不是常量表達式  
const int sz = get_size();      // sz不是常量表達式 

  儘管staff_size的初始值是個字面值常量,但由於它的數據類型只是一個普通int而非const int,所以它不屬於常量表達式。另一方面,儘管sz本身是一個常量,但它的具體值直到運行時才能獲取到,所以也不是常量表達式。

  在一個複雜系統中,很難(幾乎肯定不能)分辨一個初始值到底是不是常量表達式。當然可以定義一個const變量並把它的初始值設爲我們認爲的某個常量表達式,但在實際使用時,儘管要求如此卻常常發現初始值並非常量表達式的情況。可以這麼說,在此種情況下,對象的定義和使用根本就是兩回事兒。

  C++11新標準規定,允許將變量聲明爲constexpr類型以便由編譯器來驗證變量的值是否是一個常量表達式。聲明爲constexpr的變量一定是一個常量,而且必須用常量表達式初始化:

constexpr int mf = 20;          // 20是常量表達式  
constexpr int limit = mf + 1;   // mf + 1是常量表達式  
constexpr int sz = size();      // 只有當size是一個onstexpr函數時纔是一條正確的聲明語句 

  儘管不能使用普通函數作爲constexpr變量的初始值,但是,新標準允許定義一種特殊的constexpr函數。這種函數應該足夠簡單以使得編譯時就可以計算其結果,這樣就能用constexpr函數去初始化constexpr變量了。

  一般來說,如果你認定變量是一個常量表達式,那就把它聲明成constexpr類型。

  常量表達式的值需要在編譯時就得到計算,因此對聲明constexpr時用到的類型必須有所限制。因爲這些類型一般比較簡單,值也顯而易見、容易得到,就把它們稱爲”字面值類型”(literal type)。

  到目前爲止接觸過的數據類型中,算術類型、引用和指針都屬於字面值類型。自定義類Sales_item、IO庫、string類型則不屬於字面值類型,也就不能被定義成constexpr。

  儘管指針和引用都能定義成constexpr,但它們的初始值卻受到嚴格限制。一個constexpr指針的初始值必須是nullptr或者0,或者是存儲於某個固定地址中的對象。

  值得一提的是,函數體內定義的變量一般來說並非存放在固定地址中,因此constexpr指針不能指向這樣的變量。相反的,定義於所有函數體之外的對象其地址固定不變,能用來初始化constexpr指針。同樣,允許函數定義一類有效範圍超出函數本身的變量(即局部靜態變量),這類變量和定義在函數體之外的變量一樣也有固定地址。因此,constexpr引用能綁定到這樣的變量上,constexpr指針也能指向這樣的變量。

  • 指針和constexpr

必須明確一點,在constexpr聲明中如果定義了一個指針,限定符constexpr僅對指針有效,與指針所指的對象無關:

const int *p = nullptr;         // p是一個指向整型常量的指針  
constexpr int *q = nullptr;     // q是一個指向整數的常量指針 

  p和q的類型相差甚遠,p是一個指向常量的指針,而q是一個常量指針,其中的關鍵在於constexpr把它所定義的對象置爲了頂層const。

與其他常量指針類似,constexpr指針既可以指向常量也可以指向一個非常量:

constexpr int *np = nullptr;    // np是一個指向整數的常量指針,其值爲空  
int j = 0;  
constexpr int i = 42;       // i的類型是整型常量  
// i和j都必須定義在函數體之外  
constexpr const int *p = &i;    // p 是常量指針,指向整型常量i  
constexpr int *p1 = &j;         // p1是常量指針,指向整數j 

參考


函數、函數指針、函數對象、lambda表達式

函數指針

是指向函數的指針變量,在C編譯時,每一個函數都有一個入口地址,那麼這個指向這個函數的函數指針便指向這個地址。
函數指針的用途是很大的,主要有兩個作用:用作調用函數和做函數的參數。
函數指針的聲明方法:
數據類型標誌符 (指針變量名) (形參列表);
一般函數的聲明爲:
int func ( int x );
而一個函數指針的聲明方法爲:
int (*func) (int x);
前面的那個(*func)中括號是必要的,這會告訴編譯器我們聲明的是函數指針而不是聲明一個具有返回型爲指針的函數,後面的形參要視這個函數指針所指向的函數形參而定。
然而這樣聲明我們有時覺得非常繁瑣,於是typedef可以派上用場了,我們也可以這樣聲明:

typedef int (*PF) (int x);
PF pf;

這樣pf便是一個函數指針,方便了許多。當要使用函數指針來調用函數時,func(x)或者 (*fucn)(x) 就可以了,當然,函數指針也可以指向被重載的函數,編譯器會爲我們區分這些重載的函數從而使函數指針指向正確的函數。

typedef void (*PFT) ( char ,int );
void bar(char ch, int i)
{
    cout<<"bar "<<ch<<' '<<i<<endl;
    return ;
}
PFT pft;
pft = bar;
pft('e',91);

函數指針另一個作用便是作爲函數的參數,我們可以在一個函數的形參列表中傳入一個函數指針,然後便可以在這個函數中使用這個函數指針所指向的函數,這樣便可以使程序變得更加清晰和簡潔,而且這種用途技巧可以幫助我們解決很多棘手的問題,使用很小的代價就可獲得足夠大的利益(速度+複雜度)。

函數對象

前面是函數指針的應用,從一般的函數回調意義上來說,函數對象和函數指針是相同的,但是函數對象卻具有許多函數指針不具有的優點,函數對象使程序設計更加靈活,而且能夠實現函數的內聯(inline)調用,使整個程序實現性能加速。
函數對象:這裏已經說明了這是一個對象,而且實際上只是這個對象具有的函數的某些功能,我們才稱之爲函數對象,意義很貼切,如果一個對象具有了某個函數的功能,我們變可以稱之爲函數對象。
可以向一個算法傳遞任何類型的可調用對象。可調用對象有函數、函數指針、lambda表達式、重載了函數調用運算符的類即函數對象。

class A{
public:
int operator()(int x){return x;}
};
A a;
a(5);

原文博客

lambda表達式

匿名函數

 lambda函數是一個依賴於實現的函數對象類型,這個類型的名字只有編譯器知道. 如果用戶想把lambda函數做爲一個參數來傳遞, 那麼形參的類型必須是模板類型或者必須能創建一個std::function類似的對象去捕獲lambda函數.使用 auto關鍵字可以幫助存儲lambda函數,

auto my_lambda_func = [&](int x) { /*...*/ };
auto my_onheap_lambda_func = new auto([=](int x) { /*...*/ });

一個沒有指定任何捕獲的lambda函數,可以顯式轉換成一個具有相同聲明形式函數指針.所以,像下面這樣做是合法的:

auto a_lambda_func = [](int x) { /*...*/ };
void(*func_ptr)(int) = a_lambda_func;
func_ptr(4); //calls the lambda.

lambda使用


STL三種智能指針

  • unique_ptr
    只允許基礎指針的一個所有者。unique_ptr則無拷貝語義,但提供了移動語義。 除非你確信需要 shared_ptr,否則請將該指針用作 POCO 的默認選項。 可以移到新所有者,但不會複製或共享。 替換已棄用的 auto_ptr。 與 boost::scoped_ptr 比較。 unique_ptr 小巧高效,大小等同於一個指針且支持 rvalue 引用,從而可實現快速插入和對 STL 集合的檢索。

  • shared_ptr
    採用引用計數的智能指針。 如果你想要將一個原始指針分配給多個所有者(例如,從容器返回了指針副本又想保留原始指針時),請使用該指針。 直至所有 shared_ptr 所有者超出了範圍或放棄所有權,纔會刪除原始指針。 大小爲兩個指針:一個用於對象,另一個用於包含引用計數的共享控制塊。

  • weak_ptr
    結合 shared_ptr 使用的特例智能指針。 weak_ptr 提供對一個或多個 shared_ptr 實例擁有的對象的訪問,但不參與引用計數。 如果你想要觀察某個對象但不需要其保持活動狀態,請使用該實例。 在某些情況下,用於斷開 shared_ptr 實例間的循環引用。

shared_ptr:

參考

智能指針類將一個計數器與類指向的對象相關聯,引用計數跟蹤該類有多少個對象共享同一指針。

  • 每次創建指針類的新對象時,初始化指針並將引用計數置爲1;
  • 當指針對象作爲另一指針對象的副本而創建時,拷貝構造函數拷貝指針並增加與之相應的引用計數;
  • 對一個指針對象進行賦值時,賦值操作符減少左操作數所指對象的引用計數(如果引用計數爲減至0,則刪除對象),並增加右操作數所指對象的引用計數;
  • 調用指針析構函數時,減少引用計數(如果引用計數減至0,則刪除所指基礎對象)。

shared_ptr實現細節:

利用一個輔助類來管理指針的複製。原來的類中有一個指針指向輔助類,輔助類的數據成員是一個計數器和一個基礎對象指針


unique_ptr基本操作:

unique_ptr“唯一”擁有其所指對象,同一時刻只能有一個unique_ptr指向給定對象(通過禁止拷貝語義、只有移動語義來實現)。

unique_ptr指針本身的生命週期:從unique_ptr指針創建時開始,直到離開作用域。離開作用域時/reset函數時,若其指向對象,則將其所指對象銷燬(默認使用delete操作符,用戶可指定其他操作)。

unique_ptr指針與其所指對象的關係:在智能指針生命週期內,可以改變智能指針所指對象,如創建智能指針時通過構造函數指定、通過reset方法重新指定(原對象被銷燬)、通過release方法釋放所有權(原對象不會被銷燬)、通過移動語義轉移所有權。

//智能指針的創建  
unique_ptr<int> u_i; //創建空智能指針”
u_i.reset(new int(3)); //"綁定”動態對象  
unique_ptr<int> u_i2(new int(4));//創建時指定動態對象  

//所有權的變化  
int *p_i = u_i2.release(); //釋放所有權  
unique_ptr<string> u_s(new string("abc"));  
unique_ptr<string> u_s2 = std::move(u_s); //所有權轉移(通過移動語義),u_s所有權轉移後,變成“空指針”  
u_s2=nullptr;//顯式銷燬所指對象,同時智能指針變爲空指針。與u_s2.reset()等價  

weak_ptr:

當一個weak_ptr所觀察的shared_ptr要釋放它的資源時,它會把相關的weak_ptr的指針設置爲空,防止weak_ptr持有懸空的指針。

weak_ptr並不擁有資源的所有權,所以不能直接使用資源

weak_ptr並沒有重載-> 和 * 操作符,所以我們不能通過他來直接使用資源,我們可以通過lock來獲得一個shared_ptr對象來對資源進行使用,如果引用的資源已經釋放,lock()函數將返回一個存儲空指針的shared_ptr。 expired函數用來判斷資源是否失效。

shared_ptr<int> sp(new int(4));
weak_ptr<int> wp(sp);
if (!wp.expired())
{
    shared_ptr<int> sp2 = wp.lock();
    *sp2 = 100;
}

應用:
weak_ptr用於解決循環引用問題


虛函數表實現細節

參考1

參考2

成員函數指針

派生類新增虛函數會增加到非虛主基類虛函數表中。

編譯器只知道pb是B*類型的指針,並不知道它指向的具體對象類型 :pb可能指向的是B的對象,也可能指向的是D的對象。

但對於“pb->bar()”,編譯時能夠確定的是:此處operator->的另一個參數是B::bar(因爲pb是B*類型的,編譯器認爲bar是B::bar),而B::bar和D::bar在各自虛函數表中的偏移位置是相等的。

無論指針/引用指向哪種類型的對象,只要能夠確定被調函數在虛函數中的偏移值,待運行時,能夠確定具體類型,並能找到相應vptr了,就能找出真正應該調用的函數。

函數成員指針與普通函數指針相比,其size爲普通函數指針的兩倍(x64下爲16字節),分爲:ptr和adj兩部分。

虛函數成員指針ptr部分內容爲虛函數對應的函數指針在虛函數表中的偏移地址加1(之所以加1是爲了用0表示空指針),而adj部分爲調節this指針的偏移字節數


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章