面試雜記

2020-08-12 13:34:24

RAII

全稱是 Resource Acquisition Is Initialization , 即「資源獲取即初始化」,其核心是把資源和物件的生命週期系結:物件建立獲取資源,物件銷燬釋放資源。這就是的資源也有了生命週期,有了自動回收的功能。lock_guard 都利用了 RAII機制 機製來實現。

防止記憶體泄露的方式有 RAII、智慧指針。

大端和小端

  • 大端就是高位元組在高地址,低位元組在低地址。

  • 小端就是低位元組在高地址,高位元組在低地址。

       // 大端小端區分  
       bool isBigEndian() {  
            union NUM  {  
                int a;  
                char b; // 如果是大端 b 就是最高位 ,小端就是最低位  
            }num;  
    
            num.a = 0x1234;  
            if(num.b == 0x12)  
            {  
                return true;  
            }  
    
            return false;  
        }
    
  • 大端小端轉換

    //無符號整型16位元  
    uint16_t bswap_16(uint16_t x)  {  
        return ((x & 0x00ff) << 8) | (x & 0xff00) >> 8) ;  
    }  
     
    //無符號整型32位元
    uint32_t bswap_32(uint32_t x) {  
        return ((x & 0xff000000) >> 24)| ((x & 0x00ff0000) >> 8) | \
               ((x & 0x0000ff00) << 8) | ((x & 0x000000ff) << 24) ;  
    } 
    

生成可執行檔案過程及各個過程完成的事情:

  1. 預編譯處理(.c) : 將原始檔main.cc 翻譯成一個ASCII碼的中介軟體檔案 main.i
  2. 編譯、優化程式(.s、.asm): 將 main.i檔案 翻譯成一個 ASCII 彙編檔案 main.s
  3. 彙編程式(.obj、.o、.a、.ko) :執行彙編器,將 main.s 翻譯成一個可重定位目標檔案main.o
  4. 鏈接程式(.exe、.elf、.axf等) : 執行鏈接器,將main.o 中使用到的目標檔案組合起來,建立一個可執行的檔案
    爲了構造可執行檔案,這鏈接器必須完成兩個主要任務:
    • 符號解析 :目的是將每個符號參照正好和一個符號定義關聯起來。每個符號對應於一個函數、全域性變數、static變數等
    • 重定位:對於由編譯器和彙編器生成的從地址0開始的程式碼和數據節,鏈接器將每個符號定義與一個記憶體位置關聯起來,從而重定位這些數據節,然後修改所有對這些符號的參照,使得他們指向記憶體位置

靜態庫與動態庫

根本區別:是在編譯期還是在是執行期完成鏈接、裝入動作。鏈接的主要內容就是把各個模組之間相互參照的部分都處理好,
使得各個模組之間能夠正確地銜接

  • 靜態庫:之所以叫做靜態庫,是因爲在鏈接階段,當使用鏈接器將由彙編生成的目標檔案構造成一個可執行的輸出檔案時,它只是複製靜態庫中裡這個即將生成的可執行檔案所參照到的目標模組。靜態庫特點總結:

    • 靜態庫對函數庫的鏈接是放在編譯時期完成的。

    • 程式在執行時與函數庫再無瓜葛,移植方便。

    • 浪費空間和資源,因爲所有相關的目標檔案與牽涉到的函數庫被鏈接合成一個可執行檔案。

  • 動態庫:動態庫在程式編譯時並不會被連線到目的碼中,而是在程式執行是才被載入。不同的應用程式如果呼叫相同的庫,那麼在記憶體裡只需要有一份該共用庫的範例,規避了空間浪費問題。動態庫在程式執行是才被載入,也解決了靜態庫對程式的更新、部署和發佈頁會帶來麻煩。使用者只需要更新動態庫即可,增量更新

    • 動態庫把對一些庫函數的鏈接載入推遲到程式執行的時期。

    • 可以實現進程之間的資源共用。(因此動態庫也稱爲共用庫)

    • 將一些程式升級變得簡單。

    • 甚至可以真正做到鏈接載入完全由程式設計師在程式程式碼中控制(顯示呼叫)。

編譯型語言和直譯語言的區別

  • 編譯型語言在執行前就生成可執行檔案,執行時就沒有編譯了
  • 直譯語言在執行的時候才翻譯

static、extern、全域性變數

  • static全域性變數與普通的全域性變數有什麼區別:static全域性變數只初使化一次,防止在其他檔案單元中被參照;

  • static區域性變數和普通區域性變數有什麼區別:static區域性變數只被初始化一次,下一次依據上一次結果值;

  • 程式的區域性變數存在於(堆疊)中,全域性變數存在於(靜態區 )中,動態申請數據存在於( 堆)中。

    extern全域性變數(用extern修飾的變數只是說明該變數在其他地方定義,所以在其他地方一定要用明確的定義如int a,並且不能用static修飾)、static全域性變數和static區域性變數的生存期都是「永久」,區別只是可見域不同。extern全域性變數可見區域是工程,static全域性變數可見區域是檔案,而static區域性變數的可見區域是塊。

volatile

  • 阻止編譯器爲了提高速度將一個變數快取到暫存器內而不寫回。

  • 阻止編譯器調整操作volatile變數的指令順序。

    注意:即使 volatile 能夠阻止編譯器調整順序, 也無法阻止CPU動態排程換序(reorder

assert

斷言主要用於檢查邏輯上不可能的情況。例如,它們可用於檢查程式碼在開始執行之前所期望的狀態,或者在執行完成後檢查狀態。與正常的錯誤處理不同,斷言通常在執行時被禁用

assert 是個宏而非函數,如果條件返回錯誤,則會拋出異常,最後會呼叫 abort 終止程式,發送的是 SIGABRT,可以通過宏 NODEBUG 來關閉 assert,但是需要設定在原始碼的開頭。

#define assert(expr)								\
     (static_cast <bool> (expr)						 \
      ? void (0)									\
      : __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))

extern 
void __assert_fail (const char *__assertion, 
                    const char *__file,
                    unsigned int __line, 
                    const char *__function)
{
     __THROW __attribute__ ((__noreturn__));
}

在判斷失敗時會呼叫 __assert_fail 來拋出異常,在C++中異常的底層是用 abort 實現的。

指針和參照的區別

  • 目的不同:指針是爲了相容 C 而存在,參照是爲了操作符過載而存在
  • 在宣告參照的的同時就要對它初始化,並且一經宣告就不可以再和其它物件系結在一起了。
  • 參照更像是常數指針,只能改變系結物件的值,不能改變系結的物件。
  • 參照它一定不爲空,因此相對於指針,它不用檢查它所指物件是否爲空,更加安全也增加了效率

new、malloc

  • new:不僅僅是分配記憶體,還包括了物件型別轉換以及初始化

    • 是先呼叫operator new分配記憶體空間,返回是void *型別;
    • 再將返回型別轉換爲指定型別,再呼叫類別建構函式。
    • 如果記憶體空間不足,會拋出std::bad_alloc異常
  • malloc

    • 返回 void*型別,並且在記憶體不足時,返回NULL指針

    • 當開闢的空間小於 128K時,呼叫brk() 函數,malloc的底層實現是系統呼叫函數 brk()

    • 當開闢的空間大於 128K時,mmap() 系統呼叫函數來在虛擬地址空間中(堆和棧中間,稱爲「檔案對映區域」的地方)找一塊空間來開闢

      operator new 底層也是由malloc實現。 malloc底層是由slab實現。

      對於POD型別物件,使用new 建立的物件,也是可以使用free來釋放銷燬物件,原因就是在於這個物件是POD 型別。沒有其他資源需要釋放,或者檔案描述符需要關閉,因此可以不呼叫解構函式而使用free來替代delete。儘管可以,但是不推薦,這樣的程式碼並不健壯。

      new 在C++裡是可以用 malloc + placement 替代的,或者說等效於這兩個。

      // 更好的展示 malloc 與 new 的區別與聯繫
      class Foo { 
      public:
          Foo(int a=0, int b=0): a(a),b(b) { }
      
          ~Foo() { std::cout<<"dtor"<<std::endl; }
      private:
          int a;
          int b;
      };
      
      int main(int argc, char const *argv[])
      {
          // new = placement new + malloc
          Foo* foo = static_cast<Foo*>(::malloc(sizeof(Foo)));
          new(foo)Foo(1,1);
          
          // delete
          foo->~Foo();
          ::free(foo);
          return 0;
      }
      // 執行結束,無記憶體泄露
      
  • 除了newmalloc,還有什麼方式可以在堆中分配記憶體麼,以及怎麼釋放?
    mmapmunmap

多型

多型的三個條件:繼承,重寫(override),基礎類別參照指向派生類物件。

  • 靜態多型

    • 過載,在編譯時期就可以確定

    • 模板技術:比如 CRTP,它是使用子類來作爲基礎類別的模板參數,在基礎類別中呼叫子類的方法。

      template<typename Derived>
      class Base{ 
            // 其他忽略
          Derived& convert() 
          { 
          	return static_cast<Derived&>(*this);
          }
      };
      

      這個也從側面說明static_cast 可以讓子類轉換爲父類別,要使得保證安全,前提是轉換後呼叫子類的方法中沒有使用子類獨有的數據成員

  • 動態多型
    執行時才確定呼叫的是哪個函數。核心關鍵在於虛擬函式:子類重寫基礎類別的虛方法,定義指向子類物件的基礎類別指針。這個基礎類別指針的行爲直到執行時才能 纔能確定呼叫的是子類還是基礎類別的方法,這就是多型。

    實現原理是:虛擬函式表和虛擬函式指針vptr詳情

過載(overload)和重寫(override)

  • 過載:允許多個同名函數,而這些函數的參數列表不同,函數模板這麼實現的,在編譯期間就能確定。

    C++函數過載底層實現原理是C++利用 name mangling 技術,來改變函數名,區分參數不同的同名函數。編譯器通過函數名和其參數型別識別過載函數。不能僅僅基於不同的返回型別而實現函數過載,是因爲經過 name mangling 後得到的函數名與返回值型別是無關的。

      void func(int a) { }
    
      int func(int a, int b) { return a+b; }
    
      int main(int argc, char const *argv[])
      {
        return 0;
      }
    

    比如,如上的程式碼。在經過編譯後,得到的符號表如下:

      $ objdump -t main.o
      0000000000000000 g     F .text  000000000000000e _Z4funci
      000000000000000e g     F .text  0000000000000018 _Z4funcii
      0000000000000026 g     F .text  0000000000000016 main
    

    其中, 字首 __z 是規定,4 是函數名的字元個數,i是第一個函數的參數型別intii是第二個函數的參數型別int, int。由此可見也是與返回值無關的。

  • 重寫override:是指子類重新定義父類別的方法,子類的函數名和參數列表以及返回值必須與父類別的完全一致。對於虛擬函式的重寫,c++11中新定義了一個關鍵詞override,就是爲了讓子類在重寫父類別的虛擬函式方法時,如何參數列表發生更改可以讓編譯器報錯。

static

static變數都是在全域性數據區分配記憶體,宣告週期直到程式執行結束.

  • 四類static

    • 全域性static變數和static函數:都已經被匿名名稱空間取代,作用是不能外部檔案使用
    • 區域性static變數:在數據區.data分配記憶體,首次初始化以後,以後呼叫都不會再初始化,作用域僅侷限於函數,生命週期直到程式執行結束
    • 靜態數據成員:類物件共用,不能在類宣告中定義,是因爲在定義時就要分配空間,也是在.data
    • 靜態成員函數:靜態成員函數,不隱含this指針,不能呼叫非靜態成員函數/變數,可以用作回撥函數
  • 爲什麼要引入static

    需要一個數據物件爲整個類而非某個物件服務,同時又不能破壞類的封裝特性,因此將靜態成員隱藏在類的內部,提供靜態成員函數介面,因爲共用,可以節省記憶體。

物件

  • 空類有六個成員函數

      class Empty { 
      public:
        Empty(); 
        Empty(const Empty& );
        ~Empty();
    
        Empty& operator=(consy Empty& );
        Empty* operator& ();               // 取地址運算子
        const Empty* operator& () const;   // const 型別取地址運算子
      };
    
  • 建構函式可以是虛擬函式嗎?解構函式可以是虛擬函式嗎?

    虛擬函式對應一個vtbl,這個vtbl實際是儲存在物件的記憶體空間.如果建構函式是虛的,物件的構造就需要vtbl來呼叫,而虛擬函式表又是在物件構造後才能 纔能建立,因此建構函式不能是虛擬函式.

    而解構函式在使用多型的繼承中一般都是虛解構函式.爲的是能正確選擇解構函式.

  • c++的深拷貝如何理解

    在類中有指針時並且內部分配資源.經過淺拷貝後,最終會造成資源一次分配,多次釋放.造成系統崩潰.

C++中如何在main()函數之前執行操作

main函數執行之前,主要就是初始化系統相關資源:

  • 設定棧指針
  • 初始化靜態static變數和global全域性變數,即.data段的內容
  • 將未初始化部分的全域性變數賦初值:數值型shortintlong等爲0boolFALSE,指針爲NULL等等,即.bss段的內容
  • 全域性物件初始化,在main之前呼叫建構函式
  • 將main函數的參數argcargv等傳遞給main函數,然後才真正執行main函數

main函數執行之後

  • 全域性物件的解構函式會在main函數之後執行;
  • 可以用 atexit 註冊一個函數,它會在main 之後執行;

sizeof 和 strlen 區別

  int main(int argc, char const *argv[]){
      
      const char* str = "name";

      sizeof(str); // 取的是指針str的長度,是8
      strlen(str); // 取的是這個字串的長度,不包含結尾的 \0。大小是4
      return 0;
  }

strcpy、strncpy和mmemcpy的區別

char* strcpy(char*  dest, const char* src);
char* strncpy(char* dest, const char* src, size_t n);
void* memcpy (void* dest, const void* src, size_t n);

前面兩個函數是以字元爲單位,而mmemcpy是以位元組爲單位。

strcpymemcpy主要有以下3方面的區別。

  • 複製的內容不同。strcpy 只能複製字串,而memcpy可以複製任意內容,例如字元陣列、整型、結構體、類等。
  • 複製的方法不同。strcpy 不需要指定長度,它遇到被複制字元的串結束符'\0'才結束,所以容易溢位。memcpy 則是根據其第3個參數決定複製的長度,而且如果字串數據中包含'\0',只能用memcpy
  • 用途不同。通常在複製字串時用strcpy,而需要複製其他型別數據時則一般用memcpy

strncpy :在複製字串時,memcpy更加類似於strncpy
strncpymemcpy很相似,只不過它在一個終止的空字元處停止。當 n > strlen(src) 時,dst[strlen(len)] ~ dst[n-1]都是\0;當 n<=strlen(src)時,複製前src的n個字元。這裏隱藏了一個事實,就是dst指向的記憶體一定會被寫n個字元。

memcpy需要注意的是:

  • dest 指針要分配足夠的空間,也即大於等於 n 位元組的空間。如果沒有分配空間,會出現斷錯誤。
  • destsrc 所指的記憶體空間不能重疊(如果發生了重疊,使用 memmove() 會更加安全)。

動手實現 memcpy

void* myMemcpy(void* dst, const void* src, size_t n) { 
  if(dst ==nullptr || src==nullptr) return nullptr;
  if(src == dst) retrun src;
    
    char* pdst       = static_cast<char*>(dst); 
    const char* psrc = static_cast<const char*>(src);
    // 發生重疊時,從後向前複製
    if(psrc <  pdst && pdst < psrc + n) 
    { 
        for(int i=n-1; i >=0; --i)  pdst[i] = psrc[i];
    }
    else 
    { 
        for(int i=0; i < n; ++i) pdst[i] = psrc[i];
    }
    return pdst;
}

// 使用
int main(int argc, char const *argv[])
{
    char buf[12]={0};
    char str[]  = "hello world cpp";

    myMemcpy(str, str+6, 9);
    std::cout<<str<<std::endl;
    return 0;
}

野指針和懸空指針

都是是指向無效記憶體區域(這裏的無效指的是"不安全不可控")的指針,存取行爲將會導致未定義行爲。

  • 野指針
    野指針,指的是沒有被初始化過的指針

    int main(void) { 
        
        int* p;     // 未初始化
        std::cout<< *p << std::endl; // 未初始化就被使用
        
        return 0;
    }
    

    因此,爲了防止出錯,對於指針初始化時都是賦值爲 nullptr,這樣在使用時編譯器就會直接報錯,產生非法記憶體存取。

  • 懸空指針
    懸空指針,指針最初指向的記憶體已經被釋放了的一種指針。

    int main(void) { 
      int * p = nullptr;
    
      int* p2 = new int;
      
      p = p2;
    
      delete p2;
    }
    

此時 p和p2就是懸空指針,指向的記憶體已經被釋放。繼續使用這兩個指針,行爲不可預料。需要設定爲p=p2=nullptr。此時再使用,編譯器會直接保錯。

避免野指針比較簡單,但懸空指針比較麻煩。c++引入了智慧指針,C++智慧指針的本質就是避免懸空指針的產生。

malloc、calloc、realloc

    #include <stdlib.h>

    void *malloc(size_t size);
    void *calloc(size_t nmemb, size_t size);
    void *realloc(void *ptr, size_t size);
    void free(void *ptr);
  • malloc
    最常用的分配記憶體函數,分配size個位元組。分配的記憶體是未初始化的,如果size==0,要麼返回NULL,要麼返回一個獨一無二的指針,能被free釋放。

  • realloc:用來改變已有記憶體區的大小,而不改變內容。新的大小爲參數size,即newsize。

    • ptr==NULLrealloc(NULL,size)就相當於malloc(size)
    • size==0realloc(ptr, 0)就相當於free(ptr)
    • ptr==NULL && size==0:危險。
    • 如果newsize > oldsize,那麼增加的記憶體是未初始化的,原來的記憶體內容保持不變,即[ptr, ptr+oldsize)內部不變,[ptr+oldsize, ptr+newsize)是初始化的內容。
  • 如果newsize < oldsize,尾部的內容就被切去,釋放,只是剩下前面。

    因此 realloc之後不能給這個記憶體區初始化.

  • calloc
    分配nmemb個元素,每個元素大小是size個位元組的連續記憶體。

    • 記憶體大小是nmemb*size的連續記憶體區
    • 這塊記憶體被初始化爲0

    如果size==0,要麼返回NULL,要麼返回一個獨一無二的指針,能被free釋放。

編譯知識

爲了減小編譯依賴加快編譯速度和生成二進制檔案的大小,C/C++ 專案中一般在 *.h 檔案對於指針型別(包括智慧指針) 儘量使用前置宣告,而不是直接包含對應類的標頭檔案。例如:

  //Test.h
  //在這裏使用A的前置宣告,而不是直接包含A.h檔案
  class A;

  class Test
  {
  public:
    Test();
    ~Test();

  private:
    A*
  };

Copy Elision

g++ 編譯器是預設開啓 copy elison 選項的。如果要關閉這個選項,使用 -fno-elide-constructorscopy elision 主要包括以下兩項內容:

1. 返回值優化

即通過將返回物件所佔空間的直接構造到他們本來要複製/移動到的物件中去,依次來避免拷貝/移動操作。返回值優化包括具名返回值優化 NRVO 與 匿名返回值優化 URVO,區別在於返回值是具名的區域性變數(NRVO)還是無名的臨時物件(URVO

  • URVO 與 徹底 Copy elision

      class Copyable { 
    
      public:
        Copyable() { std::cout<<"default ctor"<<std::endl; }
    
        Copyable(const Copyable& rhs) = delete; 
        Copyable(Copyable&& rhs) = delete;
      };
    
      Copyable return_urvo_value() { 
    
        return Copyable{}; // since c++17 ok
      }
    
      int main(int argc, char const *argv[]) {
    
        auto x  = return_urvo_value();
        
        return 0;
      }
    

    上述程式碼在C++17中是可以編譯通過的。在 C++17 之前,並沒有明確的提出在什麼情況下,可以徹底進行 Copy Elision(這裏的徹底的指的是包括不進行檢查是否有可用的 copy/move 建構函式)。在C++17中,對於匿名物件(or 臨時物件)不論是傳遞參數,還是以返回值返回時,都不會呼叫拷貝/移動構造。因此上面的這段程式碼在C++17是可以正常編過的,而在C++14會編譯出錯。

      $ g++ main.cc -std=c++14 -o main && ./main
      main.cc: In function ‘Copyable return_urvo_value():
      main.cc:29:19: error: use of deleted function ‘Copyable::Copyable(Copyable&&)29 |   return Copyable{};
           |                   ^
      main.cc:24:3: note: declared here
        24 |   Copyable(Copyable&& rhs) = delete;
           |   ^~~~~~~~
      main.cc: In function ‘int main(int, const char**):
      main.cc:34:31: error: use of deleted function ‘Copyable::Copyable(Copyable&&)34 |   auto x  = return_urvo_value();
           |                               ^
      main.cc:24:3: note: declared here
        24 |   Copyable(Copyable&& rhs) = delete;
           |   ^~~~~~~~
    

    自然,只要將上面程式碼中的如下兩行註釋掉,即可正常編譯,並且 Copyable的建構函式都是隻被呼叫一次,即copy elision 起作用了。 注意:Copyable的複製/移動建構函式必須同時可存取。

      Copyable(const Copyable& rhs) = delete; 
      Copyable(Copyable&& rhs) = delete;
    

    因此,在C++17以前,對於 urvo 不在乎是否返回的物件的複製/移動建構函式是否存在或者可存取,copy elision 都能生效。而在 C++14 之前,返回的物件可以沒有複製/移動建構函式,但是必須可以存取。

  • nrvo
    nrvo時,返回物件的複製/移動建構函式必須可存取。否則編譯不過。

      class Copyable { 
      public:
        Copyable() { std::cout<<"default ctor"<<std::endl; }
    
        Copyable(const Copyable& rhs) = delete;
        Copyable(Copyable&& rhs) = delete;
      };
    
      Copyable return_urvo_value() { 
    
        return Copyable{};
      }
    
      Copyable return_nrvo_value() { 
        Copyable local;
    
        return local;
      }
    
      int main(int argc, char const *argv[]) {
    
        auto x  = return_urvo_value();
        auto y  = return_nrvo_value();
        
        return 0;
      }
    

    如上程式碼,即使是C++17也會編譯失敗,必須將如下兩行程式碼註釋掉,使得 Copyable 物件的複製/移動建構函式可存取。copy elision 才能 纔能生效:Copyable 的預設建構函式只調用一次。

      // Copyable(const Copyable& rhs) = delete;
      // Copyable(Copyable&& rhs) = delete;
    

2. 右值拷貝優化

右值拷貝優化,當某一個類的臨時物件以值傳遞給該類的另一個物件時,也可以直接利用該臨時物件的來避免拷貝/移動操作。在上面的基礎上,加上如下的程式碼:

  void pass_by_value(Copyable rhs) { 

  }

  int main(int argc, char const *argv[]) {

    auto x  = return_urvo_value();
    auto y  = return_nrvo_value();

    pass_by_value(Copyable());
    
    return 0;
  }

最終的輸出也是呼叫預設三次建構函式:

  $ g++ main.cc -std=c++11 -o main && ./main
  default ctor
  default ctor
  default ctor

到此,copy elision 基本分析結束。如果想檢視沒有copy elision 作用下的輸出,開啓-fno-elide-constructors

Copy Elision 作用

對於一些沒有拷貝/移動構造的物件,如 unique_ptratomic 等。現在我們能夠定義一個工廠函數,即使沒有複製或移動建構函式都可以返回一個物件。例如,以下通用工廠函數:

  template <typename T, typename... Args>
  T make_instance(Args&& ... args)
  {
    return T{ std::forward<Args>(args)... };
  }
  
  int main()
  {
    int i   = make_instance<int>(42);
    // std::unique_ptr 實現了 移動建構函式,因此可以編譯成功 
    auto up = make_instance<std::unique_ptr<int>>(new int{ 42 }); 
    // 禁止了複製建構函式,但是也沒有實現移動建構函式,因此要到 C++17 才能 纔能編譯過
    auto ai = make_instance<std::atomic<int>>(42);                  
  
    return 0;
  }

參考

STL

std::vector

  • std::vectorpush_back 以成倍增長可以在均攤後達到O(1)的事件複雜度,相對於增長指定大小的O(n)時間複雜度更好。
  • 爲了防止申請記憶體的浪費,現在使用較多的有2倍與1.5倍的增長方式,而1.5倍的增長方式可以更好的實現對記憶體的重複利用,因爲更好
  • std::vector/std::list的實現原理及其使用場景
    std::vector是用連續記憶體的陣列實現的,std::list是通過指針之間的指向實現不連續記憶體的高效率使用.

std::vectorstd::list

  • 陣列,最大的好處是能以 O(1) 用索引存取任意元素,次要好處是記憶體佈局緊湊,省記憶體之餘還有高快取一致性(cache coherence)。但陣列的缺點是不能快速插入元素,而且我們在解析 JSON 陣列的時候,還不知道應該分配多大的陣列才合適。
  • 鏈表,它的最大優點是可快速地插入元素(開端、末端或中間),但需要以 O(n) 時間去經索引取得內容。如果我們只需順序遍歷,那麼是沒有問題的。還有一個小缺點,就是相對陣列而言,鏈表在儲存每個元素時有額外記憶體開銷(儲存下一節點的指針),而且遍歷時元素所在的記憶體可能不連續,令快取不命中(cache miss)的機會上升。

std::bind

對於如下程式碼,使用 std::bind 系結類的成員函數並呼叫。

class Foo { 
public:
  Foo()=default;

  void add(const int& lhs, const int& rhs) 
  { 
    std::cout<< (lhs + rhs)<<std::endl;;
  }
};

int main(int argc, char const *argv[])
{
  Foo foo1;
   // 系結並且執行
  std::bind(&Foo::add, &foo1, 1, 2)();
  return 0;
}

最後內部執行的程式碼如下:

template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_deref, _MemFun&& __f, _Tp&& __t, _Args&&... __args)
{
  return ((*std::forward<_Tp>(__t)).*__f)(std::forward<_Args>(__args)...);
}

其中

  • _t ,型別是 Foo*& 函數物件指針,指向的foo1物件。
  • _f是函數指針,指向的就是add成員函數
  • __invoke_memfun_deref:是用來標記是哪種把系結方式,比如上述程式碼中的系結物件成員函數

因此,最終,呼叫可以簡化爲如下:

foo1.add(1,2);

整個std::bind 最終的執行程式碼如下:

// 直接傳入函數呼叫: std::bind(func, arg1, arg2); 或者靜態成員函數
// 很明顯,這裏沒有類物件
template<typename _Res, typename _Fn, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_other, _Fn&& __f, _Args&&... __args)
{ return std::forward<_Fn>(__f)(std::forward<_Args>(__args)...); }

// 上面介紹過
template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_deref, _MemFun&& __f, _Tp&& __t, _Args&&... __args)
{
  return ((*std::forward<_Tp>(__t)).*__f)(std::forward<_Args>(__args)...);
}

// 下面 下麪幾種沒見過呼叫
template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_ref, _MemFun&& __f, _Tp&& __t, _Args&&... __args)
{ return (__invfwd<_Tp>(__t).*__f)(std::forward<_Args>(__args)...); }

template<typename _Res, typename _MemPtr, typename _Tp>
constexpr _Res
__invoke_impl(__invoke_memobj_ref, _MemPtr&& __f, _Tp&& __t)
{ return __invfwd<_Tp>(__t).*__f; }

template<typename _Res, typename _MemPtr, typename _Tp>
constexpr _Res
__invoke_impl(__invoke_memobj_deref, _MemPtr&& __f, _Tp&& __t)
{ return (*std::forward<_Tp>(__t)).*__f; }

Lambda

lambda可以理解爲是仿函數的語法糖。

int main(int argc, char const *argv[])
{
  auto add  = [](int a, int b) { return a+b; };
  int c = add(1,2);

  return 0;
}

對於上面的lambda函數,在gdb偵錯,經過編譯的到的函數型別:

(gdb) s
<lambda(int, int)>::operator()(int, int) const (__closure=0x7fffffffe6d3, a=1, b=2) at main.cpp:24
24        auto add  = [](int a, int b) { return a+b; };

lambda可以看作是匿名的函數物件,並且 lambda表達式預設是 const屬性。

class Foo { 
public:
  Foo() 
  { 
    [](){std::cout<<"123"<<std::endl; }();
  }
};

int main(int argc, char const *argv[])
{ 
  Foo foo;

  return 0;
}

在類中呼叫lambda表達式,編譯出來的型別如下:

Foo::Foo()::{lambda()#1}::operator()() const

而實際上,lambda與operator本質上也是一樣的,如下程式碼:

	class Foo { 
    public:
      Foo() 
      { 
        // 以下兩個設計等價
        [this](const char* str){this->print(str); }("lambda");
        Unmaed(this)("operator");
      }

      void print(const char* str) { 
        std::cout<<str<<std::endl;
      }

    private:
      struct Unmaed
      {
        Unmaed(Foo* foo): foo_(foo) { }

        void operator()(const char* str) const
        {
          foo_->print(str);
        }

        Foo* foo_;
      };
    };

std::bind 與 lambad區別

  • lambda 可以過載,但是 std::bind 無法區別過載

      void f(int) {}
    void f(double) {}
    
      auto g = [] { f(1); }; // OK
      auto g = std::bind(f, 1); // 錯誤
      auto g = std::bind(static_cast<void(*)(int)>(f), 1); // OK
    

爲此必須指定對應的函數指針型別。lambda 閉包類的 operator() 採用的是能被編譯器內聯的常規的函數呼叫。而std::bind採用的是一般不會被內聯的函數指針呼叫,這意味着 lambdastd::bind 執行得更快

  • 傳給 std::bind 的參數,系結的是 std::bind,而不是std::bind內部管理的函數
    void f(std::chrono::steady_clock::time_point t, int i)
    {
        std::this_thread::sleep_until(t);
        std::cout << i;
    }
    
    auto g = [](int i)
    {
        f(std::chrono::steady_clock::now() + std::chrono::seconds(3), i);
    };
    
    g(1); // 3秒後列印1
    // 用std::bind實現相同效果,但存在問題
    auto h = std::bind(f,
                        std::chrono::steady_clock::now() + 	 std::chrono::seconds(3),
                        std::placeholders::_1);
    
    h(1); // 3秒後列印1,但3秒指的是呼叫std::bind後的3秒,而非呼叫f後的3秒
    

計算時間的表達式作爲實參被傳遞給std::bind,因此計算髮生在呼叫std::bind的時刻,而非呼叫其系結的函數的時刻。

c++14 中,完全沒有理由使用 std::bindc++11由於特性受限,存在兩個使用場景:

  • 模擬c++11缺少的移動捕獲
  • 函數物件 operator() 是模板時,如果將此函數作爲參數使用,用 std::bind 系結才能 纔能接受任意型別參數
struct X {
    template<typename T>
    void operator()(const T&) const;
};

X x;
auto f = std::bind(x, _1); // f可以接受任意參數型別

// c++14 做法
X a;
auto f = [a](const auto& x) { a(x); };

Lambdastd::bind 區別補充

std::bind 傳入的參數預設情況下是 「值傳遞」,想要使用參照傳遞需要std::ref。詳細可以參考下面 下麪的程式碼:

  class Foo { 
  public:
    Foo() 
    { 
      std::cout<<"default"<<std::endl; 
    }

    Foo(const Foo& rhs)     
    { 
      std::cout<<"ctor"<<std::endl;
    }
  };

  void add(const Foo& lhs, const Foo& rhs) { 

  }


  int main(int argc, char const *argv[])
  {
    std::cout<<std::boolalpha;

    Foo foo1;
    Foo foo2;
  
    std::cout<<"bind: pass by value"<<std::endl;
    auto func = std::bind(add,  foo1, foo2);
    
    std::cout<<"bind: pass by ref"<<std::endl;
    auto func = std::bind(add,  std::ref(foo1), std::ref(foo2));

    std::cout<<"lambda "<<std::endl;
    [&foo1, &foo2]{ add(foo1, foo2);}();

    return 0;
  }

上面的程式碼輸出:

  $ g++ -g  -O0 main.cc -o main && ./main
  default
  default
  bind: pass by value
  ctor
  ctor
  bind: pass by ref
  lambda 

可以看到std::bind在預設情況下,是依靠值傳遞,使用了std::ref來包裹傳入參數纔是使用參照傳遞。用 gdb 偵錯,可以跟蹤到發生構造 Foo 物件的位置:

template<typename _UHead>
constexpr _Head_base(_UHead&& __h) : _Head(std::forward<_UHead>(__h)) { }

// 整個呼叫鏈如下:
#0  Foo::Foo (this=0x7fffffffe068, rhs=...) at main.cc:13
#1  0x0000555555555a8c in std::_Head_base<1ul, Foo, true>::_Head_base<Foo&> (this=0x7fffffffe068, __h=...) at /usr/include/c++/9/tuple:87
#2  0x00005555555559e8 in std::_Tuple_impl<1ul, Foo>::_Tuple_impl<Foo&> (this=0x7fffffffe068, __head=...) at /usr/include/c++/9/tuple:349
#3  0x00005555555558f3 in std::_Tuple_impl<0ul, Foo, Foo>::_Tuple_impl<Foo&, Foo&, void> (this=0x7fffffffe068, __head=...)
    at /usr/include/c++/9/tuple:218
#4  0x0000555555555815 in std::tuple<Foo, Foo>::tuple<Foo&, Foo&, true> (this=0x7fffffffe068, __a1=..., __a2=...) at /usr/include/c++/9/tuple:969
#5  0x00005555555556fc in std::_Bind<void (*(Foo, Foo))(Foo const&, Foo const&)>::_Bind<Foo&, Foo&>(void (*&&)(Foo const&, Foo const&), Foo&, Foo&)
    (this=0x7fffffffe060, __f=@0x7fffffffe010: 0x5555555551ea <add(Foo const&, Foo const&)>) at /usr/include/c++/9/functional:467
#6  0x0000555555555571 in std::bind<void (&)(Foo const&, Foo const&), Foo&, Foo&> (__f=
    @0x5555555551ea: {void (const Foo &, const Foo &)} 0x5555555551ea <add(Foo const&, Foo const&)>) at /usr/include/c++/9/functional:812
#7  0x00005555555552b7 in main (argc=1, argv=0x7fffffffe198) at main.cc:30

左值參照和右值參照區別

左值參照,也就是「常規參照」,不能系結到要轉換的表達式,字面常數,或返回右值的表達式。而右值參照恰好相反,可以系結到這類表達式,但不能系結到一個左值上。

右值參照就是必須系結到右值的參照,通過&&獲得。右值參照只能系結到一個將要銷燬的物件上,因此可以自由地移動其資源。

返回左值的表達式包括返回左值參照的函數及賦值,下標,解除參照和前置遞增/遞減運算子
返回右值的包括返回非參照型別的函數及算術,關係,位和後置遞增/遞減運算子。可以看到左值的特點是有持久的狀態,而右值則是短暫的

 void func(int&& index, int idx) { 
   if(idx > 3) return;

   func(index++, ++idx);  // Ok 
   func(++index, ++idx);  // Error

   std::cout<< std::is_rvalue_reference<decltype(index)>::value <<std::endl;
 }

在上面的程式碼中,++index 產生的是左值,而 index++ 產生的是右值。因此上面的可以編譯成功,下面 下麪的編譯不過。

【注意】:已經命名的右值,編譯器會認爲是左值。

左值與右值

變數都是左值,即使變數是右值參照型別

  int&& ref1 = 1;     //  ok 
  int&& ref2 = ref1; // error 

因爲 ref2 是右值參照型別的變數,不能將其系結到左值ref1上。ref1ref2 是左值,因爲他們都是變數,但是變數型別是右值參照型別,即這兩個變數只能系結到右值上

四種類型轉換總結

static_cast

  • 基礎類別和子類之間轉換:
    static_cast 的使用,當且僅當型別之間可隱式轉化時,static_cast 的轉化纔是合法的。有一個例外,那就是類層次間的向下轉型,static_cast 可以完成類層次間的向下轉型,但是向下轉型無法通過隱式轉換完成。

    • 向上轉換安全:子類指針轉換成父類別指針是安全的;

    • 向下轉換不安全:父類別指針轉換成子類指針是不安全的。

    • static_cast不能進行無關型別(如非基礎類別和子類)指針之間的轉換。

    class Base{ };
    class Derived : public base{ /**…*/ };

      Base*    B = new Base;
      Derived* D = static_cast<Drived*>(B); // 不安全
    ```
    爲什麼不安全?   
    
    D指向本質上還是B的物件模型,D指向的記憶體模型中可能存在B沒有的成員變數。如果 `D->foo()` 中使用了 `D` 的成員變數,那麼這個函數呼叫就是不安全的。因此,向下轉換是安全的。
    
  • static_cast 還可以在左值和右值之間顯示地轉換。雖然不能隱式地將左值轉換爲右值,但是可以使用static_cast顯示地將左值轉換爲右值。

  • 基本數據型別轉換: enum, int, char, float等。安全性問題由開發者來保證。

  • 把空指針轉換成目標型別的空指針

        int* iptr = static_cast<int*>(::malloc(sizoef(int)));
    
  • 把任何型別的表達式轉換成void型別:static_cast<void>(iptr)

  • static_cast 不能去掉型別的const、volitale屬性(用const_cast)

  • 隱式轉換都建議使用 static_cast 進行標明和替換

dynamic_cast

專門用於將多型基礎類別的指針或參照強制轉換爲派生類的指針或參照,而且能夠檢查轉換的安全性。對於不安全的指針轉換,轉換結果返回 nullptr 指針。

使用特點:

  • 基礎類別必須要有虛擬函式,因爲dynamic_cast是執行時型別檢查,需要執行時型別資訊,而這個資訊是儲存在類的虛擬函式表中,只有一個類定義了虛擬函式,纔會有虛擬函式表

  • 對於下行轉換,dynamic_cast是安全的(當型別不一致時,轉換過來的是空指針),而static_cast是不安全的(當型別不一致時,轉換過來的是錯誤意義的指針,可能造成踩記憶體,非法存取等各種問題), reinterpreter_cast 下行轉換是可以轉換,但是不安全。

  • 相同基礎類別不同子類之間的交叉轉換,轉換結果是是 nullptr

      class Base
      {
      public: 
        virtual void fun() { } 
      };
    
      class Drived : public base {
      public:
        int i;
      };
    
      Base     *Bptr = new Drived()//語句0
      Derived *Dptr1 = static_cast<Derived*>(Bptr);  //語句1;
      Derived *Dptr2 = dynamic_cast<Derived*>(Bptr); //語句2;
    

此時語句1和語句2都是安全的,因爲此時 Bptr 確實是指向的派生類的記憶體模型,所以兩個型別轉換都是安全的。Dptr1Dptr2 可以盡情存取 Drived 類中的成員,絕對不會出問題。但是如果此時語句0更改爲如下表達:

  Base* Bptr = new Base(); `

那麼 Bptr 指向的是Base物件記憶體模型。因此語句1是不安全的,因爲如果存取子類的數據成員,其行爲將是未定義。而語句2返回的是 nullptr,更加直觀的告訴使用者不安全。

reinterpreter_cast

用於進行各種不同類型的指針之間、不同類型的參照之間以及指針和能容納指針的整數型別之間的轉換。轉換時執行的是byte 複製的操作。

  • reinterpret_cast是從底層對數據僅僅進行重新解釋,但沒有進行二進制的轉換,依賴具體的平臺,可移植性差;
  • reinterpret_cast可以將整型轉換爲指針,也可以把指針轉換爲陣列;
  • reinterpret_cast可以在指針和參照裡進行肆無忌憚的轉換;

const_cast

  • 常數指針轉換爲非常數指針, 並且仍然指向原來的物件
  • 常數參照被轉換爲非常數參照,並且仍然指向原來的物件

自己實現一個非侵入式的智慧指針

// RAII 技術封裝  
class RefCount {
public:
      RefCount() : reference_{0} 
      { }

      ~RefCount() { 
          this->decrementRef();
      }

      void IncrementRef()
      {
          ++reference_;
      }

      bool decrementRef()
      {
          if (--reference_ == 0) {
            delete this;
            return true;
          }
          return false;
      }
      
      int64_t use_count() {
          return reference_;
      }

  private:
      std::atomic<int64_t> reference_;
  };


  template <typename T>
  class SharedPtr
  {
  public:
      SharedPtr() : ptr_(nullptr) {}

      explicit SharedPtr(T* ptr) 
      : ptr_(ptr),
        ref_(new RefCount)
      {
          if (ref_) ref_->IncrementRef();
      }

      SharedPtr(const SharedPtr& other) 
      : ptr_(other.ptr_),
        ref_(other.ref_)
      {
          if (ref_) ref_->IncrementRef();
      }

      SharedPtr(SharedPtr&& other) noexcept {
          ptr_ = other.ptr_;
          ref_ = other.ref_;
          other.ptr_ = nullptr;
          other.ref_ = nullptr;
      }

      SharedPtr& operator=(const SharedPtr& other) {
          if (this == &other || *this == other) 
              return *this;

          reset();
          ptr_ = other.ptr_;
          ref_ = other.ref_;
          ref_->IncrementRef();
          return *this;
      }

      SharedPtr& operator=(SharedPtr&& other) noexcept {
          if (this == &other || *this == other) return *this;

          reset();
          ptr_ = other.ptr_;
          ref_ = other.ref_;
          
          other.ptr_ = nullptr;
          other.ref_ = nullptr;

          return *this;
      }

      ~SharedPtr() 
      {
          if (ref_) this->decrementRef(); 
      }

      T& operator*()  const { return *ptr_; }
      T* operator->() const { return ptr_; }

      explicit operator bool() const { return !!ptr_; }

      T* get() const { return ptr_; }

      void reset() {
          if (ptr_) {
              this->decrementRef();
              // ptr_ = nullptr;
          }
      }

      void decrementRef() { 
          if(ref_ && ptr_) { 
              if(ref_->decrementRef()) { 
                  delete ptr_;
                  ptr_ = nullptr;
              }
          }
      }

      int64_t use_count() {
          return  ref_->use_count();
      }

      bool unique() {
          return use_count() == 1;
      }

      void swap(SharedPtr & other) {
          std::swap(ptr_, other.ptr_);
          std::swap(ref_, other.ref_);
      }

      friend inline bool operator==(SharedPtr const& lhs, SharedPtr const& rhs) {
          return lhs.ptr_ == rhs.ptr_;
      }

      friend inline bool operator!=(SharedPtr const& lhs, SharedPtr const& rhs) {
          return lhs.ptr_ != rhs.ptr_;
      }

      friend inline bool operator<(SharedPtr const& lhs, SharedPtr const& rhs) {
          return lhs.ptr_ < rhs.ptr_;
      }

  private:
      T*        ptr_; 
      RefCount* ref_;
  };

  int main(int argc, char const *argv[]) {
    
      SharedPtr<int> iptr (new int);
      SharedPtr<int> iptr2(iptr);
      SharedPtr<int> iptr3(std::move(iptr));
      SharedPtr<int> iptr4 = iptr2;
      SharedPtr<int> iptr5 = std::move(iptr3);

      std::cout<<iptr5.use_count()<<std::endl; // 3
      return 0;
  }

迭代器失效的場景

  • 序列式容器
    序列式容器會失效的原因是因爲其儲存都是連續的,因此刪除或者插入一個元素都有可能導致其他元素的迭代器失效。

    • vector

      • 在遍歷時,執行erase會導致刪除節點之後的全部失效
      • push_back時,之前的end()操作得到的迭代器失效
      • insert/push_back導致capacity()改變,那麼之前的first()/end()得到的迭代器會失效
    • insert一個元素,如果空間沒有分配,那麼插入節點之前的迭代器位置有效,之後的失效。

      簡而言之:導致記憶體分配的全會失效,導致元素移動的會區域性失效

    • deque

      • 在首尾新增元素,會導致迭代器失效,但是指針、參照不會失效
      • 其餘位置插入元素,迭代器、指針、參照都是失效
      • 在首尾之外的位置刪除元素,那麼其他位置的迭代器都失效
      • 在首尾刪除元素,只是會導致被指向的刪除元素的迭代器失效
  • 關聯式容器

    • 基於雜湊表實現的std::unordered_map/std::set 導致迭代器失效,一般是插入元素導致 reshash 產生,如果是刪除只是會導致被刪除元素的迭代器失效。
      [外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-gSPsHN7u-1597210398000)(image/unorder_map非法化.jpg)]

std::unordered_map

底層實現及其解決 hash 衝突方法

std::unorder_map 解決衝突方式是 拉鍊法(陣列的每個元素都連着一個鏈表?):將所有產生衝突的關鍵字所對應的數據全部儲存在同一個線性鏈表中(bucket)。這個方法解決數據儲存位置發生衝突的雜湊表,整個儲存結構如圖 1 所示。

其中 p_i表示儲存的各個鍵值對。

當使用無序容器儲存鍵值對時,會先申請一整塊連續的儲存空間,但此空間並不用來直接儲存鍵值對,而是儲存各個鏈表的頭指針,各鍵值對真正的儲存位置是各個鏈表的節點。STL 標準庫預設選用vector容器儲存各個鏈表的頭指針。STL 標準庫中,將圖 1 中的各個鏈表稱爲桶 bucket,每個桶都有自己的編號(從 0 開始)。當有新鍵值對儲存到無序容器中時,整個儲存過程分爲如下幾步:

  • 將該鍵值對中 key 的值帶入設計好的雜湊函數,會得到一個雜湊值: H = hash(key)
  • 將 H 和無序容器擁有桶的數量 n 做整除運算(即H % n),該結果即表示應將此鍵值對儲存到的桶的編號;
  • 建立一個新節點儲存此鍵值對,同時將該節點鏈接到相應編號的桶上

其他解決衝突的方法

  • 開放地址法:
    H=(hash(key)+d)%m H =(hash(key) + d) \% m

    其中m是雜湊表的表長,d是一個增量,當產生衝突時,選擇以下三種方法一種獲取d的值,然後計算,直到計算出的hash 值不存在衝突。

    • 線性探測法:d = 1,2,3,…
    • 二次探測法:d = 12,-12, 22, -22…
    • 僞亂數探測法: d = 僞亂數
  • 再雜湊法rehasp
    當通過雜湊函數求得的雜湊地址同其他關鍵字產生衝突時,使用另一個雜湊函數計算,直到衝突不再發生

rehashp

上述的拉鍊法解決了雜湊衝突的問題,但是當插入元素很多,產生了嚴重雜湊衝突時,就會導致某個鏈表長度越來越長,進而導致雜湊表的查詢就會退化爲鏈表,效率降低爲O(n)的時間複雜度。

雜湊表儲存結構還有一個重要的屬性,稱爲負載因子load factor,用於衡量容器儲存鍵值對的空/滿程度:即負載因子越大,意味着容器越滿,即各鏈表中掛載着越多的鍵值對,這無疑會降低容器查詢目標鍵值對的效率;反之,負載因子越小,容器肯定越空,但並不一定各個鏈表中掛載的鍵值對就越少。

負載因子的計算方法爲:負載因子 = 容器儲存的總鍵值對 / 桶數

STL 中預設情況下,無序容器的最大負載因子爲 1.0。如果操作無序容器過程中,使得最大複雜因子超過了預設值,則容器會自動增加桶數,並重新進行雜湊,以此來減小負載因子的值。需要注意的是,此過程會導致容器迭代器失效,但指向單個鍵值對的參照或者指針仍然有效。

因此當插入元素過多,使得負載因子的大於1.0,就會產生rehash行爲,來改善即將下降的效率。

STL中的 unordered_maprehash 策略
一下程式碼節選自g++的STL庫:

// __n_bkt is current bucket count, __n_elt is current element count,
// and __n_ins is number of elements to be inserted.  Do we need to
// increase bucket count?  If so, return make_pair(true, n), where n
// is the new bucket count.  If not, return make_pair(false, 0).
std::pair<bool, size_t>
 _M_need_rehash(size_t __n_bkt, size_t __n_elt, size_t __n_ins) noexcept {
  if (__n_elt + __n_ins >= _M_next_resize) {
      
    long double __min_bkts = (__n_elt + __n_ins) / (long double)_M_max_load_factor;
      
    if (__min_bkts >= __n_bkt)
      //  這個是需要rehash 時返回的策略
      return std::make_pair(true, 
                            _M_next_bkt(std::max<size_t>(__builtin_floor(__min_bkts) + 1,
                                                         __n_bkt * _S_growth_factor)));

        _M_next_resize  = __builtin_floor(__n_bkt * (long double)_M_max_load_factor);

        return std::make_pair(false, 0);
      }
     
      return std::make_pair(false, 0);
    }

size_t _M_next_bkt(size_t __n) noexcept {
    
  const auto __max_width = std::min<size_t>(sizeof(size_t), 8);           // 8 個位元組
  const auto __max_bkt   = size_t(1) << (__max_width * __CHAR_BIT__ - 1); // 2 ^ 63
  size_t __res           = __clp2(__n);        // 計算大於等於 n 的最小的2的冪

  if (__res == __n) __res <<= 1;
  if (__res == 0) __res = __max_bkt;

  // Set next resize to the max value so that we never try to rehash again
  // as we already reach the biggest possible bucket number.
  // Note that it might result in max_load_factor not being respected.
  if (__res == __max_bkt)
    _M_next_resize = size_t(-1);
  else
    _M_next_resize = __builtin_ceil(__res * (long double)_M_max_load_factor);

  return __res;
}

其中,在這個類的前面有定義:

  • _M_max_load_factor 初始化爲 1.0
  • static const size_t _S_growth_factor = 2;

整個擴容的策略大致是按照2倍的策略增長,但是並不嚴格按照。在MSVC中按照的是8倍的擴充策略。

多執行緒下的 std::unordered_map

STL中的 雜湊表 是執行緒不安全的(其實 STL 庫都是執行緒不安全的)。 比如兩個執行緒同時向 std::unordered_map 中插入數據,當發生rehash時,如果不加鎖,可能導致兩個執行緒都會產生rehash

如何優化 多執行緒讀寫操作?這裏要借鑑下java的分段鎖。

參考鏈接

map 與 紅黑樹.

  • 紅黑樹的規則

    1. 樹根始終爲黑色
    2. 外部節點均爲黑色
    3. 其餘節點若爲紅色,則器孩子節點必爲黑色
    4. 從任一外部外部節點到根節點的沿途,黑色節點的數目相等

    條件1和2說明,紅色節點均是內部節點,其父節點及左、右孩子節點必然存在;另外條件3說明,紅節點之父必爲黑色,因此樹中任一通路都不含相鄰的紅節點

  • 紅黑樹的效率爲什麼比AVL樹高?如果只有查詢操作哪種樹的效率高?

    紅黑樹的效率高,是因爲不需要像AVL樹那樣,爲了維護高度平衡性,而不斷地動態調整以維護左右子樹高度差不超過1.紅黑樹降低了對平衡度的要求,以減少每次插入/刪除操作時的動態調整次數.但是就查詢效率而言,是不如AVL樹的.

工具

如何進行效能排查?找出效能瓶頸?效能測試?有什麼工具:

  • valgrind :可以查詢程式碼問題

GDB學習參考鏈接



void *Memcpy(void *dst, const void *src, size_t1 size)
 
{
 
    char *psrc;  //源地址
    char *pdst;  //目標地址
  
    if(NULL == dst || NULL == src)
    {
        return NULL;
    }
 
    if((src < dst) && (char *)src + size > (char *)dst)  //源地址在前,對應上述情況2,需要自後//向前拷貝
    {
        psrc = (char *)src + size - 1;
        pdst = (char *)dst + size - 1;
        while(size--)
        {
            *pdst-- = *psrc--;
        }
    }
    else   //源地址在後,對應上述第一種情況,直接逐個拷貝*pdst++ = *psrc++即可
    {
        psrc = (char *)src;
        pdst = (char *)dst;
        while(size--)
        {
            *pdst++ = *psrc++;
        }
    }
    return dst;
 
}

重寫memcpy()函數需要注意哪些問題?

答:自己動手實現memcpy()時就需要考慮地址重疊的情況。我們來看個簡單的例子。有一個5個元素的陣列,不妨設爲int arr = {1,2,3,4,5};考慮2種情況:

1)源地址是arr[2],目標地址是arr[0],自前向後拷貝3個元素後arr爲{3,4,5,4,5}

2)源地址是arr[0],目標地址是arr[2],自前向後拷貝3個元素後arr爲{1,2,1,2,3}

第一種情況,由低地址向高地址逐個將源地址的元素拷貝到目標地址就行,容易;

第二種情況需要注意,如果是按第一種情況由低地址拷貝到高地址,需要分3個步驟把arr[0]=1,arr[1]=2,arr[2]=3三個元素逐個拷貝,重點在於第一步是將arr[0]拷貝到arr[2]的位置,這樣就會把原來的arr[2]=3改爲arr[2]=1,覆蓋了原來的值,因此在這種情況,我們需要自後向前拷貝,也就是高地址向低地址拷貝。也就是第一步將arr[2]放到arr[4],第二步將arr[1]放到arr[3],第一步將arr[0]放到arr[2].


2.1.10陣列到底存放在哪裏

解析:

1、固定陣列在函數體內分配(不帶static)是在中的

2、固定陣列是全域性變數和帶static字首的區域性陣列是在全域性數據

3、固定陣列在類中分配是在堆中的

4、動態陣列(通過malloc或者new出來的空間)不管在函數體中、類中、全域性變數都是在


char和int之間的轉換;

解析:

1)首先char與int都分爲signed與unsigned型別,預設情況下都是signed型別。

2)從長位元組數據型別轉換爲短位元組數據型別,會產生截斷:

如從4位元組的int型別轉換成1個位元組的char型別,則取int數據的最低的一個位元組,將這個位元組的數據賦給char型數據,且是有符號的,即首位爲符號位;而如果是從int轉換成unsigned char型別,則整個一個位元組都是數據,沒有符號位。

1)從短位元組型別轉換爲長位元組型別,從char轉換爲int:則在前面的三個位元組補符號位,即補上0xffffff(char的首位爲1),或0x000000(char的首位爲0)。從unsigned char轉換爲int,則前面補上0x000000.


static的用法(定義和用途)

解析:

在C語言中,static作用:「改變生命週期」 或者 「改變作用域」。有以下特性:

1)static區域性變數:區域性變數爲動態儲存,即指令執行到定義處才分配記憶體,將一個變數宣告爲函數的區域性變數,使其變爲靜態儲存方式(靜態數據區),那麼這個區域性變數在函數執行完成之後不會被釋放,而是繼續保留在記憶體中。

2)static全域性變數:全域性變數即定義{}外面,其本身就是靜態變數,編譯時就分配記憶體,這隻會**改變其連線方式,**使其只在本檔案內部有效,而其他檔案不可連線或參照該變數。

3)static函數:對函數的連線方式產生影響,使得函數只在本檔案內部有效,對其他檔案是不可見的。這樣的函數又叫作靜態函數。使用靜態函數的好處是,不用擔心與其他檔案的同名函數產生幹擾,另外也是對函數本身的一種保護機制 機製。如果想要其他檔案可以參照本地函數,則要在函數定義時使用關鍵字extern,表示該函數是外部函數,可供其他檔案呼叫。另外在要參照別的檔案中定義的外部函數的檔案中,使用extern宣告要用的外部函數即可。

到了C++的時候,static多了幾個其他的作用:

4)static類成員變數:表示這個成員爲全類所共有,對類的所有物件只有一份拷貝,可以藉助類名直接存取。

5)static類成員函數:表示這個函數爲全類所共有,而且只能存取靜態成員變數,因爲這個函數不接收this指針。


const常數和#define的區別(編譯階段、安全性、記憶體佔用等)

解析:主要有以下區別

1)用``#define MAX 255`定義的常數是沒有型別的(不進行型別安全檢查,可能會產生意想不到的錯誤),所給出的是一個立即數,編譯器只是把所定義的常數值與所定義的常數的名字聯繫起來,define所定義的宏變數在預處理****階段的時候進行替換,在程式中使用到該常數的地方都要進行拷貝替換;

用``const float MAX = 255`;定義的常數有型別****(編譯時會進行型別檢查)名字,存放在記憶體的靜態區域中,在編譯時確定其值。在程式執行過程中const變數只有一個拷貝,而#define所定義的宏變數卻有多個拷貝,所以宏定義在程式執行過程中所消耗的記憶體要比const變數的大得多;

2)用define定義的常數是不可以用指針變數去指向的,用const定義的常數是可以用指針去指向該常數的地址的;

3)用define可以定義一些簡單的函數(宏替換隻作替換,不做計算,不做表達式求解),const是不可以定義函數的.

4)宏定義的作用範圍僅限於當前檔案。 而預設狀態下,const物件只在檔案內有效,當多個檔案中出現了同名的const變數時,等同於在不同檔案中分別定義了獨立的變數。 如果想在多個檔案之間共用const物件,必須在變數定義之前新增extern關鍵字(在宣告和定義時都要加)。


volatile關鍵詞的作用是影響編譯器編譯的結果,用volatile宣告的變數表示該變數隨時可能發生變化,與該變數有關的運算,不要進行編譯優化,以免出錯。


數據庫爲什麼要建立索引,以及索引的缺點

解析:嵌入式問的不多,當作瞭解。

回憶一下小時候查字典的步驟,索引和字典目錄的概念是一致的。字典目錄可以讓我們不用翻整本字典就找到我們需要的內容頁數,然後翻到那一頁就可以。索引也是一樣,索引是對記錄按照多個欄位進行排序的一種展現。對錶中的某個欄位建立索引會建立另一種數據結構,其中儲存着欄位的值,每個值還包括指向與它相關記錄的指針。這樣,就不必要查詢整個數據庫,自然提升了查詢效率。同時,索引的數據結構是經過排序的,因而可以對其執行二分查詢,那就更快了

先說優點:

1)大大加快數據的檢索速度,這也是建立索引的最主要的原因

2)加速表和表之間的連線,特別是在實現數據的參考完整性方面特別有意義。

3)在使用分組和排序子句進行數據檢索時,同樣可以顯著減少查詢中分組和排序的時間。

再說缺點:

1)建立索引需要耗費一定的時間,但是問題不大,一般索引只要build一次

2)索引需要佔用物理空間,特別是聚集索引,需要較大的空間

3)當對錶中的數據進行增加、刪除和修改的時候,索引也要動態的維護,降低了數據的維護速度,這個是比較大的問題。


爲什麼stl裏面有sort函數list裏面還要再定義一個sort

解析:這是本人在面試CVTE遇到的面試題。

1、支援隨機存取迭代器的(連續儲存空間)vector、deque(雙向存取vector)使用STL的sort函數。

2、不支援隨機存取迭代器的(鏈式非連續儲存空間)list(雙向鏈表)、slist(單向鏈表forward_list),不能使用STL的sort函數,因此都會在類中定義sort()成員函數,使用物件名呼叫即可。

3、關係型容器中基於紅黑樹的set、multiset、map、multimap,本身就有自動從大到小排序的功能。所以不需要sort函數。

4、stack、queue沒有迭代器,各元素的出入口特定,不能進行排序。

5、基於雜湊表的(hash)unordered_set/multiset/map/multimap,都是未排序的,當然因爲計算hash再儲存的特性,也不需要進行排序。


爲什麼建立連線是三次握手,關閉連線確是四次揮手呢?

解析:建立連線的時候,伺服器在LISTEN狀態下,收到建立連線請求的SYN報文後,把ACK和SYN放在一個報文裡發送給用戶端。

而關閉連線時,伺服器收到對方的FIN報文時,僅僅表示對方不再發送數據了但是還能接收數據,而自己也未必全部數據都發送給對方了,所以己方可以立即關閉,也可以發送一些數據給對方後,再發送FIN報文給對方來表示同意現在關閉連線,因此,己方ACK和FIN一般都會分開發送,從而導致多了一次。


爲什麼用戶端最後還要等待2MSL?

解析:MSL(Maximum Segment Lifetime),TCP允許不同的實現可以設定不同的MSL值。

第一,保證用戶端發送的最後一個ACK報文能夠到達伺服器,因爲這個ACK報文可能丟失,站在伺服器的角度看來,我已經發送了FIN+ACK報文請求斷開了,**用戶端還沒有給我迴應,**應該是我發送的請求斷開報文它沒有收到,於是伺服器又會重新發送一次,而用戶端就能在這個2MSL時間段內收到這個重傳的報文,接着給出迴應報文,並且會重新啓動2MSL計時器。

第二,防止類似與「三次握手」中提到了的「已經失效的連線請求報文段」出現在本連線中。用戶端發送完最後一個確認報文後,在這個2MSL時間中,就可以使本連線持續的時間內所產生的所有報文段都從網路中消失。這樣新的連線中不會出現舊連線的請求報文。