後端c++知識點總結

這一篇是C++的一些面試點的總結。

1、一個String類的完整實現必須很快速寫出來(注意:賦值構造,operator=是關鍵)
  如果對C++String不熟悉的話,先看http://www.cplusplus.com/reference/去了解一下String類常用的方法,如果想了解C語言的實現,去看一下《C語言接口與實現》(十五章 低級字符串)。
  Scott Meyers在《effecive STL》中提到了std::string有多種實現方式。
  總結起來有三類(代碼來源《Linux多線程服務端編程》):
  
  1. 無特殊處理,使用類似std::vector的數據結構。start到end之間存儲數據,start到end_of_storage之間是容量大小,這樣能減少擴容時數據複製的概率。後面兩個成員變量還可以使用整數代替,如果字符串大小有限,可以使用u32等來表示end和end_of_storage,減小對象的大小。

class string{
    public:
        iterator begin()  { return start; }
        iterator end()    { return end;   }
    private:
        char* start;
        char* end;
        char* end_of_storage;   
};

  2. Copy-on-Write。對象裏只放一個指針。
  

class string{
    struct Rep{
        size_t size;
        size_t capacity;
        size_t refcount;
        char* data[1];
    };
    char* start;
};

  3.短字符優化,利用字符串對象本身的空間存儲短字符串,通常閾值是15字節。當定義比較短的字符串對象時,不需要再次申請分配內存。
  

class string{
    char* start;
    size_t size;
    static const int kLocalSize = 15;
    union{
        char buffer[kLocalSize+1];
        size_t capacity;
    }data;
};

這裏給出關鍵的賦值函數

/*
如下爲CMyString的聲明,請爲該類型添加賦值運算符 
*/
#include<iostream>
#include<string.h>
class CMyString{
    public:
        CMyString(char *m_pData=NULL);
        CMyString(const CMyString& str);
        ~CMyString(void);
        CMyString& operator=(const CMyString& rhs);
    private: 
        char * m_pData;
};
/*
    考察幾個知識點:
    1.返回值類型爲  X& 解決連續賦值時的左值問題
    2.使用const &提高效率,避免傳參數時的拷貝構造
    3.在賦值和拷貝構造函數中,應該先釋放原對象空間,重新申請,
    避免兩個指針指向同一內存,淺拷貝問題,避免內存泄漏。
    4.可以使用swap的寫法,先構造局部變量,在作用域之外釋放掉。
    5.字符串申請空間時。應該+1,爲結束符'\0'留位置。
    6.strcpy應該使用更安全的strncpy代替,避免緩衝區溢出。 

*/
CMyString& CMyString::operator=(const CMyString& rhs){
    if(this!=&rhs){
        delete []m_pData;
        m_pData=NULL;
        m_pData=(char *)malloc(strlen(rhs.m_pData)+1);
        strcpy(m_pData,rhs.m_pData);    
        //可用構造後交換防止異常
    /*  CMyString strTemp(rhs);
        char *pTemp=strTemp.m_pData;
        strTemp.m_pData=m_pData;
        m_pData=pTemp;*/
    }
    return *this;

}

2、虛函數的作用和實現原理(必問必考,實現原理必須很熟)

  這個問題在《深度探索C++對象模型》中有非常詳細的講解。
  這裏只簡單的說一下原理。如果你不知道虛函數和多態的概念,先去看看《C++primer》之類的語言書。
  首先,如果一個類中含有虛函數,那麼每一個類就會有一個virtual table,這個表中放置着各個虛函數的地址。然後每一個類對象(就是實例)中會被安插一個由編譯器產生的指針vptr,該指針指向一個virtual table,class所關聯的type_info,會放在vitrual table的第一個slot中,用來表示class的類型。
  識別多態的方式是查看類中是否有虛函數,實現就是通過指針取用相應的函數。例如
  

class Point{
    public:
        virtual ~Point();
        //...其他操作
        virtual float z() const { return 0; }
};
class Point3d{
    public:
        float z const{ return _z; }
    protected:
        float _z;

};
Point *ptr;
ptr = new Point3d;
ptr ->z();

當我們調用z()的時候,並不知道ptr所指向的對象的真正類型,這個是在執行期確定的。但是我們可以知道ptr可以訪問到改對象的virtual table。

雖然不知道具體哪一個z()實例會被調用,是基類的,還是繼承類的,但是我們可以確定每一個z()函數地址所存放的slot,它在virtual table中的位置是確定的。

通過這些信息編譯器可以在編譯器將該調用轉化成:

(*ptr->vptr[4])(ptr);

通過執行期獲取到ptr的具體類型,就實現了多態。

  上面是比較美好的單一繼承的情況,多繼承下需要考慮this指針的調整問題,可以選擇使用thunk技術(以適當offset調整this指針,跳到virtual function 去),或者像cfont編譯器的做法一樣,將每一個virtual table slot調整成含有offset的集合體。
  Derived class中會含有額外的virtual table來識別base1,base2…baseN。每一個virtual table的slot中放置原始的地址或者thunk地址。
  虛擬繼承還需要考慮到virtual base類的offset調整,已經繁瑣到讓人不願談了。
  

3、sizeof一個類求大小(注意成員變量,函數,虛函數,繼承等等對大小的影響)

直接給一個例子,大致覆蓋一下這些情況。

#include <iostream>
using namespace std;
class X{};
class Y:public virtual X{};
class Z:public virtual X{};
class A:public Y,public Z{};
class Point{
  private:
    char a;
    char b;
    char c;
    //註釋掉d,大小爲3
    float d;
};
class Point2d{
    public:
        virtual float getX(){ return x;}
        virtual float getY(){ return y;}
    private:
        float x;
        float y;
};
class Point3d{
    public:
        Point3d translate(const Point3d &pt){
            x += pt.x;
            y += pt.y;
        }
    private:
        float x;
        float y;
        static const int chunkSize = 250;
        float z;      
};
class Point4d:public Point3d{
    private:
        float n;
};

int main(void){
    //對於空的class,編譯器安插進一個char,使其不同實例在內存中能各自有地址
    cout << sizeof(X) << endl;
    //首先空的虛基類被視爲繼承類最開頭的一部分,空間爲0
    //而繼承類自身有一個指向表格或者virtual  base class subobject的
    //指針,這裏指針大小爲8
    cout << sizeof(Y) << endl;
    cout << sizeof(Z) << endl;
    //base Y + base Z
    cout << sizeof(A) << endl;
    cout << sizeof(void *) <<endl;
    cout << sizeof(float) <<endl;
    //注意這裏編譯器會使用對齊調整,補一個字節
    std::cout <<  sizeof(Point) << std::endl;
    //static成員並不佔用對象的空間
    std::cout <<  sizeof(Point3d) << std::endl;
    //只要有虛函數,就會生成一個vptr指針,可能在內存佈局的頭或者尾
    //大小爲vptr + 2*sizeof(float)
    std::cout << sizeof(Point2d) << std::endl;
    //繼承類會追加基類的成員
    //大小爲sizeof(base)+sizeof(drived)
    std::cout << sizeof(Point4d)  <<std::endl;
    return 0;
}

Running /home/ubuntu/workspace/gdbTest/testForData.cc
1
8
8
16
8
4
8
12
16
16

  總的來說,編譯器會自動加上一些額外的data members,用以支持某些語言特性,並會做邊界的對齊調整。這裏沒有對多重繼承,虛繼承做詳細講解,我覺得自己猜測內存佈局的情況,並且動手測一下效果更好。

4、指針和引用的區別(一般都會問到)

  已經是問爛的問題了,除了可以用來間接的引用其他對象,基本沒什麼共同點。參考《more effecive c++》
  1. 首先,引用的值不能爲空,它必須總是指向某些對象。但是記住你可以聲明一個指針變量,賦值爲空,然後把變量聲明爲引用。
  

char *pc = 0;
char &rc = *pc;

  2. 因爲引用肯定會指向一個對象,因此必須初始化,其實和1是一回事。指針無此限制,但是,其實指針的未初始化,過不了coverity這些工具的檢測。
  
  3. 指針可以重新賦值,引用不可以,初始化後就不可改變。
  
  4. sizeof結果不同,指針返回的是指針本身大小,引用返回的是引用對象的大小
  
  5.引用既然限制這麼多,爲什麼不乾脆都用指針,因爲引用不佔用空間,它只是個別名
  
如果你指向對象可能爲空或者需要改變指向的時候,使用指針,如果指向一個對象後不會改變指向或者重載某個操作符的時候,使用引用。

5、多重類構造和析構的順序

  順序就是構造時候基類的先構造,析構的時候繼承類的先析構。析構的調用順序和構造相反。

6、stl各容器的實現原理(必考)

  參考《STL源碼剖析》,或者自己用code Insight查看一下STL的類的實現。
  簡單的提一下吧。
  STL中的容器大致分爲兩類:序列式容器和關聯式容器,涵蓋了常用的各種數據結構。
  序列是指容器中的元素可序,但未必有序。除了C++提供的array之外,STL另外提供了vector,list,deque,stack,queue等。
  vector:與array相似,採用線性連續空間,但是提供了擴容的功能,當新的元素加入,且原空間不足的時候,它內部會自動分配空間,移動拷貝數據,釋放舊空間。注意vector有容量的概念,爲了防止頻繁的分配拷貝,因此申請的空間比需求的要更大一些。
  list:list相對於vector這種線性空間的好處就是利用率高,插入刪除一個元素,就分配釋放一個元素的空間,元素的插入和刪除是常數時間。STL的list就是一個環形的雙向鏈表,使用bidirection Iterators,就是具備前移,後移能力的迭代器,注意stl中的幾種不同型別迭代器。其操作就是一些指針操作。
  deque:deque是雙向開口的連續線性空間,vector也可以做成雙向的,但是其頭部的操作效率奇差,需要大量的移動後面的數據。deque通過動態的分段的連續空間的組合,完成頭端常數時間內的插入刪除操作。deque內部通過對各種指針操作的重載,完成緩衝區邊緣的處理。
  deque的大致結構如圖所示:
  這裏寫圖片描述
  stack:stack是先進後出的數據結構,只有一個出口,結構就是以deque爲底部結構,然後封閉其頭端開口,形成單向結構。這種改法稱爲適配器。
  queue:queue就是隊列了,兩個出口,先進先出。元素的操作都在頂端和底端,最頂端取出,最底端加入。結構同樣是以deque爲底部結構,然後封閉底端出口和頂端的入口。至於怎麼封閉,你不給出相對應接口函數就好了嘛。
  priority_queue:擁有權值觀念的queue。權值最高的排在隊列頭部,元素入隊的時候會按照權值排列。priority_queue缺省情況下用vector作爲容器,然後使用堆的一些泛型算法實現。

  關聯容器的每筆數據都有一個key和value。當元素插入容器時,容器內部依照鍵值的大小和某種規則將其放置到合適位置。
  STL中關聯式容器分爲set和map兩大類。
  set:set所有元素會根據鍵值自動排序,set的key就是value。並且不允許兩個元素有相同的key。set的底層就是紅黑樹,這裏不對紅黑樹做直接的介紹了。注意set的插入使用了RB-tree的insert_unique()函數來保證沒有重複的key,其結構本身是不限制key的重複的。
  multiset:和set主要的區別就是允許有重複的key值,用法和其他特性和set相同。插入操作使用insert-equal()。
  map:map的元素都是一對pair,同時擁有key和value,不允許同key。注意map的key不可更改,value可更改。map的數據類型是pair,結構底層使用RB-tree。
  multimap:類似set和multiset,差別只是插入函數的不同。
  hash_set:使用hashtable爲底層機制,就是一個vector+list的開鏈法的hash結構。
  hash_map:同樣是以hashtable爲底層,轉調用其操作。只是數據節點的類型是map而已。
  hash_multiset:和hash_set插入函數不同。
  hash_multimap:和hash_map插入函數不同。

7、extern c 是幹啥的,(必須將編譯器的函數名修飾的機制解答的很透徹)

  
  c++編譯器爲了支持重載,會對函數的名稱進行特殊的處理,在編譯函數的過程中會將函數的參數類型也添加到編譯後的代碼中。
  使用backtrace等函數打印堆棧時,就會顯示被修改過後的函數名字_Z3foov,使用c++filt可以將名稱轉化成正常的形式。
  然而C語言不支持函數的重載,編譯C語言代碼時函數不應帶上函數的參數,以此保證在C++中能夠正確調用C開發的庫,能夠找到對應的函數,使用extern C可以抑制函數的name mangling。
  
  extern C的主要含義:
  1.告訴編譯器其函數或者變量的聲明在其他模塊中,並且在鏈接階段從其他模塊中找到次函數。
  2.函數按照C語言的方式編譯和鏈接。
  

8、volatile是幹啥用的,(必須將cpu的寄存器緩存機制回答的很透徹)

  當對象的值可能在程序的控制或者檢測之外被改變時,應該將對象聲明爲volatile。告訴編譯器不應該對這樣的對象進行優化。當要求使用其聲明的變量值得時候,需要從它所在的內存地址中讀取數據,而不是從CPU的寄存器中讀取。因爲編譯器可能會將變量從內存裝入cpu寄存器中作爲緩存,提高讀取速度。
  

9、static const等等的用法,(能說出越多越好)

  static聲明外部對象時,可以達到隱藏外部對象的目的,限定其作用域爲被編譯源文件的剩餘部分。
  static聲明局部變量時,它將一直存在,不像自動變量一樣隨函數的退出而消失。static類型的內部變量時一種只能在某個特定函數中使用單一直佔據存儲空間的變量。
  const是一種語義約束,而編譯器會強制實施這項約束,它允許你告訴編譯器和其他程序員某值應該保持不變。《effictive c++》中有一條是儘可能使用const。
  const可修飾的東西非常多,常量,文件,函數,類內的static和non-static成員變量。
  在《C++primer》中使用頂層和底層const限定來區別const修飾的是指針自身還是指針所指物。
  其實很好區分,const出現在星號左邊,表示被指物是常量,出現在右邊,表示指針自身是常量。

  const char *p;//const data
  char* const p;//const pointer
  const char* const p;//const pointer,const data

  const可以在函數聲明時使用,與函數返回值,參數,函數自身產生關聯。
  1.令函數返回一個常量值,可以防止返回值被當成左值被修改。
  2.如果沒有賦值的意思,應使用const修飾參數,以區別輸入和輸出。
  3.const修飾成員函數,一是聲明那個函數可改動對象內容,二是使其可以操作const對象。注意兩個成員函數如果只是常量性不同,可以被重載。
  
  const還可以把引用綁定到const對象上,對常量的引用不能被用作修改它所綁定的對象。
  最後,使用const_cast可以解除對象的const限定。

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