C++面經總結


設計模式

參考:https://refactoringguru.cn/design-patterns/factory-method

創建型

工廠方法模式:在父類中提供一個創建對象的方法 允許子類決定實例化對象的類型

  • 創建者 (Dialog):聲明返回產品對象的工廠方法,可以作為抽象類,強制要求子類以不同的方式實現該方法
  • 具體創建者 (WindowsDialog, WebDialog):重寫基礎工廠方法,使返回不同的類型的產品
  • 產品 (Button):聲明接口
  • 具體產品 (WindowsButton, HTMLButtons):對產品接口的不同實現

 

工廠模式例子:跨平台 UI (用戶界面 組件 並同時避免客戶代碼與具體 UI 類之間的耦合,根據平台,初始化不同的具體創建者

不同的產品創建采用不同的工廠創建,這樣創建不同的產品過程就由不同的工廠分工解決:WindowDialog專心負責生產WindowButton,WebDialog專心負責生產WebButton,WindowDialog和WebDialog之間沒有關系,只為Button這個產品做接口,產品種類單一。

適用場景:

  • 適用於產品種類結構單一的場合,為一類產品提供創建的接口。
  • 如果需要向應用中添加一種新產品 你只需要開發新的創建者子類 然后重寫其工廠方法即可。便於后期產品種類的擴展。
  • 需要用戶能擴展你軟件庫或框架的內部組件
  • 需要復用現有對象來節省系統資源 而不是每次都重新創建對象 可使用工廠方法

抽象工廠模式:創建一系列相關的對象 而無需指定其具體類。抽象工廠接口聲明一系列構建方法 客戶端代碼可調用它們生成不同風格的方法

  • 抽象產品 (Button, Checkbox):構成系列產品的一組不同但相關的產品聲明接口
  • 具體產品 (WinButton, WinCheckbox, MacButton, MacCheckbox):是抽象產品的多種不同實現
  • 抽象工廠 (GUIFactory):聲明一組創建各種抽象產品的地方
  • 具體工廠 (WinFactory, MacFactory):每個工廠都對應特定產品變體,且僅創建此種產品變體

抽象模式例子:跨平台的 UI 元素 同時確保所創建的元素與指定的操作系統匹配,調用factory

讓低端工廠生產不同種類的低端產品,高端工廠生產不同種類的高端產品。讓WinFactory生產不同種類的Window UI, 讓MacFactory生產不同種類的Mac UI。產品有Button, Checckbox等等多個產品。

適用場景:

  • 需要與多個不同系列的相關產品交互。抽象工廠提供了一個接口 可用於創建每個系列產品的對象 只要代碼通過該接口創建對象 那么就不會生成與應用程序已生成的產品類型不一致的產品。適用於產品種類結構多的場合.
  • 每個類僅負責一件事 如果一個類與多種類型產品交互 就可以考慮將工廠方法抽取到獨立的工廠類或具備完整功能的抽象工廠類中

生成器模式:分步驟創建復雜對象,該模式允許使用相同的創建代碼生成不同類型和形式的對象

  • 生成器 (Builder):聲明所有類型生成器中通用的產品構造步驟
  • 具體生成器 (CarBuilder, CarManualBuilder):提供構造過程的不同實現
  • 產品 (Car, CarManual):由不同生成器構造的產品,無需屬於同一類層次結構
  • 主管 (Directior):定義構造步驟的順序,可以創建和復用特定的產品配置

生成器模式例子:復用相同的對象構造代碼來生成不同類型的產品:汽車及其相應的使用手冊,客戶端必須將生成器對象傳遞給主管對象

應用場景

  • 避免 “重疊構造函數 (telescopic constructor
  • 如果需要創建的各種形式的產品 它們的制造過程相似且僅有細節上的差異 此時可使用生成器模式
  • 生成器模式能分步驟構造產品, 這樣可以延遲執行某些步驟而不會影響最終產品

原型模式:能夠復制已有對象 而又無需使代碼依賴它們所屬的類

 

  • 原型 (Shape):接口將對克隆方法進行聲明。
  • 具體原型 (Rectangle, Circle):實現克隆方法,將原始對象的數據復制到克隆體中之外。

 

原型模式例子:生成完全相同的幾何對象副本 同時無需代碼與對象所屬類耦合

應用場景

  • 需要復制一些對象 同時又希望代碼獨立於這些對象所屬的具體類
  • 子類的區別僅在於其對象的初始化方式 那么你可以使用該模式來減少子類的數量 

單例模式:保證一個類只有一個實例 並提供一個訪問該實例的全局節點

應用場景:數據庫連接類,將緩存首次生成的對象 並為所有后續調用返回該對象。常用於管理資源

結構型

適配器:能夠轉換對象接口 能使接口不兼容的對象能夠相互合作

  • 適配器(SquarePegAdapter):可以同時與客戶端和服務端交互的類,現客戶端接口的同時封裝了服務對象
  • 服務 (SquareReg):其中有與客戶端和其接口不兼容的類,無法直接調用
  • 客戶端接口 (RoundPeg):描述了其他類與客戶端代碼合作必須遵循的協議

客戶端代碼只需通過接口與適配器交互即可 無需與具體的適配器類耦合。因此 你可以向程序中添加新類型的適配器而無需修改已有代碼

 

 

適配器例子:適配器假扮成一個Round­Peg

由於RoundHole不能直接調用SquareReg,用SquarePegAdapter封裝SquarePeg對象,返回RoundPeg類型,使RoundHole可以用。

應用場景:

  • 希望使用某個類 但是其接口與其他代碼不兼容時 可以使用適配器類
  • 需要復用這樣一些類 他們處於同一個繼承體系 並且他們又有了額外的一些共同的方法 但是這些共同的方法不是所有在這一繼承體系中的子類所具有的共性

橋接模式:可將一個大類或一系列緊密相關的類拆分為抽象和實現兩個獨立的層次結構 從而能在開發時分別使用

  • 抽象部分 (Remote):提供高層控制邏輯 依賴於完成底層實際工作的實現對象
  • 精確抽象部分 (AdvancedRemote):提供控制邏輯的變體
  • 實現部分 (Device):為所有具體實現聲明通用接口 
  • 具體實現 (Radio, TV):包括特定於平台的代碼

 

橋接例子:拆分程序中同時管理設備及其遙控器的龐雜代碼, 可以開發獨立於設備類的遙控器類 只需新建一個遙控器子類即可

設備Device類作為實現部分,而遙控器Remote類則作為抽象部分。遙控器基類聲明了一個指向Device對象的引用成員變量。 所有遙控器通過通用設備接口與設備進行交互, 使得同一個遙控器可以支持不同類型的Device。

應用場景:

  • 想要拆分或重組一個具有多重功能的龐雜類 (例如能與多個數據庫服務器進行交互的類 可以使用橋接模式

行為模式 

面向對象三大特性

  • 封裝:把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
  • 多態:允許將子類類型的指針賦值給父類類型的指針,重載和覆蓋
  • 繼承:使用現有類的所有功能,並在無需重新編寫原來的類的情況下對這些功能進行擴展

野指針

定義:

  • 指針指向了一塊沒有訪問權限的內存。(即指針沒有初始化)
  • 指針指向了一個已經釋放的內存。

避免:

  • 將指針初始化為NULL,用完后也可以將其賦值為NULL。這樣做在代碼出現段錯誤時,有利於找到錯誤並修改。
  • 使用malloc分配內存, 分配后要進行檢查是否分配成功,最后要進行釋放內存。

堆棧區別 (問過很多次了)

  • 管理方式不同:棧 (stack) 由操作系統自動分配釋放;堆 (heap) 由程序員申請和釋放,容易產生內存泄漏;
  • 空間大小不同:每個進程擁有的棧 (stack) 的大小要遠遠小於堆 (heap) 的大小;Linux默認為1M,Windows默認為2M。
  • 分配方式不同:棧 (stack) 有2種分配方式:靜態分配和動態分配。靜態分配是由操作系統完成的,比如局部變量的分配。動態分配由alloca函數進行分配,但是棧 (stack) 的動態分配和堆 (heap) 是不同的,他的動態分配是由操作系統進行釋放,無需手動實現。堆都是動態分配的。
  • 分配效率不同:棧 (stack) 的效率比堆 (heap) 的好。棧 (stack) 由操作系統自動分配,會在硬件層級對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆 (heap) 是由C/C++提供的庫函數或運算符來完成申請與管理,實現機制較為復雜,頻繁的內存申請容易產生內存碎片。
  • 存放內容不同:棧 (stack) 存放的內容,函數返回地址、相關參數、局部變量和寄存器內容等。堆 (heap),一般情況堆頂使用一個字節的空間來存放堆的大小,而堆中具體存放內容是由程序員來填充。

參數傳遞

  1. 按值傳遞:函數接收到了傳遞過來的參數后,將其拷貝一份,其函數內部執行的代碼操作的都是傳遞參數的拷貝。按值傳參最大的特點就是不會影響到傳遞過來的參數的值,但因為拷貝了一份副本,會更浪費資源一些。
  2. 按引用傳參:對傳入參數進行一些修改的時候
  3. 按常量引用傳參:讀取參數的值,而並不需要去修改它。節省拷貝開支的優點,又擁有按值傳參的不影響原值的優點
  4. 右值引用傳參:存儲的是臨時的將要被摧毀的資源,移動一個對象的狀態總會比賦值這個對象的狀態要來的簡單(開銷小)

智能指針

原理:防止忘記釋放指針造成內存泄漏,智能指針自動調用析構函數,析構函數會自動釋放資源

auto_ptr: 

對auto_ptr進行賦值時,如ptest2 = ptest,ptest2會接管ptest原來的內存管理權,ptest會變為空指針,如果ptest2原來不為空,則它會釋放原來的資源。基於這個原因,應該避免把auto_ptr放到容器中,因為算法對容器操作時,很難避免STL內部對容器實現了賦值傳遞操作,這樣會使容器中很多元素被置為NULL。已被摒棄,為了避免潛在的內存崩潰問題。

int main() {
  auto_ptr<string> films[5] =
 {
  auto_ptr<string> (new string("Fowl Balls")),
  auto_ptr<string> (new string("Duck Walks")),
  auto_ptr<string> (new string("Chicken Runs")),
  auto_ptr<string> (new string("Turkey Errors")),
  auto_ptr<string> (new string("Goose Eggs"))
 };
 auto_ptr<string> pwin;
 pwin = films[2]; // films[2] loses ownership. 將所有權從films[2]轉讓給pwin,此時films[2]不再引用該字符串從而變成空指針

for(int i = 0; i < 5; ++i)
  cout << *films[i] << endl; // 此時程序會崩潰,因為films[2]已經是空指針了,下面輸出訪問空指針當然會崩潰了
 cout << "The winner is " << *pwin << endl; 

unique_ptr:

獨享所有權,無法進行復制構造,無法進行復制賦值操作。兩個unique_ptr指向同一個對象,像上面的auto_ptr賦值是不支持會報錯的,但是可以進行移動構造和移動賦值操作

int main()
{
    unique_ptr<Test> ptest(new Test("123"));
    unique_ptr<Test> ptest2(new Test("456"));
    ptest->print();
    ptest2 = ptest // 不允許 會報錯
    ptest2 = unique_ptr<Test>(new Test("123"); // 允許,因為這里調用的是 unique_ptr 的構造函數,該構造函數創建的臨時對象在其所有權讓給 ptest2 后就會被銷毀。
    ptest2 = std::move(ptest);//不能直接ptest2 = ptest
    if(ptest == NULL)cout<<"ptest = NULL\n";
    Test* p = ptest2.release();
    p->print();
    ptest.reset(p);
    ptest->print();
    ptest2 = fun(); //這里可以用=,因為使用了移動構造函數
    ptest2->print();
    return 0;

share_ptr

使用計數機制來表明資源被幾個指針共享, 當調用release()時,當前指針會釋放資源所有權,計數減一。當計數等於0時,資源會被釋放。可以安全地放到標准容器中。要學會手寫share_ptr

/**********
* cite from: https://www.cnblogs.com/buerdepepeqi/p/12461343.html
**********/
#include <iostream>
#include <cstdlib>
using namespace std;

template <typename T>
class SmartPointer{
public:
    SmartPointer(T* ptr){
        ref = ptr;
        ref_count = (unsigned*)malloc(sizeof(unsigned));
        *ref_count = 1;
    }
    
    SmartPointer(SmartPointer<T> &sptr){
        ref = sptr.ref;
        ref_count = sptr.ref_count;
        ++*ref_count;
    }
    
    SmartPointer<T>& operator=(SmartPointer<T> &sptr){
        if (this != &sptr) {
            if (--*ref_count == 0){
                clear();
                cout<<"operator= clear"<<endl;
            }
            
            ref = sptr.ref;
            ref_count = sptr.ref_count;
            ++*ref_count;
        }
        return *this;
    }
    
    ~SmartPointer(){
        if (--*ref_count == 0){
            clear();
            cout<<"destructor clear"<<endl;
        }
    }
    
    T getValue() { return *ref; }
    
private:
    void clear(){
        delete ref;
        free(ref_count);
        ref = NULL; // 避免它成為迷途指針
        ref_count = NULL;
    }
   
protected:    
    T *ref;
    unsigned *ref_count;
};

weak_ptr:

用來解決shared_ptr相互引用時的死鎖問題,如果說兩個shared_ptr相互引用,那么這兩個指針的引用計數永遠不可能下降為0,資源永遠不會釋放。它是對對象的一種弱引用,不會增加對象的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過調用lock函數來獲得shared_ptr。

兩個不同進程的指針有可能指向同一個地址

共享內存允許兩個或更多進程訪問同一塊內存,要注意的是多個進程之間對一個給定存儲區訪問的互斥

new/delete v.s. free/malloc 

  1. 屬性:new/delete是C++關鍵字,需要編譯器支持。malloc/free是庫函數,需要頭文件支持。
  2. 參數:使用new操作符申請內存分配時無須指定內存塊的大小,編譯器會根據類型信息自行計算。而malloc則需要顯式地指出所需內存的尺寸。
  3. 返回類型:new操作符內存分配成功時,返回的是對象類型的指針,類型嚴格與對象匹配,無須進行類型轉換,故new是符合類型安全性的操作符。而malloc內存分配成功則是返回void * ,需要通過強制類型轉換將void*指針轉換成我們需要的類型。
  4. 異常:new內存分配失敗時,會拋出bad_alloc異常。malloc分配內存失敗時返回NULL。
  5. 自定義類型:malloc/free是庫函數,只能動態的申請和釋放內存,無法強制要求其做自定義類型對象構造和析構工作。
  6. 內存區域:new操作符從由c++默認使用堆實現的自由存儲區(free store)上為對象動態分配內存空間,而malloc函數從堆上動態分配內存。

new 相對於 malloc的優點

  1. 不需要提前知道內存申請的大小,因為new 內置了sizeof、類型轉換和類型安全檢查功能
  2. new是類型安全的,而malloc不是
  3. new可以重載,可以自定義內存分配策略,甚至不做內存分配,甚至分配到非內存設備上

this指針

定義:this作用域是在類內部,指向被調用函數所在的類實例的地址。當在類的非靜態成員函數中訪問類的非靜態成員的時候,編譯器會自動將對象本身的地址作為一個隱含參數傳遞給函數

this指針創建時間:this在成員函數的開始執行前構造,在成員的執行結束后清除

this指針存放在何處:this指針會因編譯器不同而有不同的放置位置,可能是棧,也可能是寄存器,甚至全局變量。

this指針是如何傳遞類中的函數的:大多數編譯器通過ecx寄存器傳遞this指針。

this指針只有在成員函數中才有定義。因此,你獲得一個對象后,也不能通過對象使用this指針。所以,我們無法知道一個對象的this指針的位置

虛函數

為什么要虛析構函數基類采用virtual虛析構函數是為了防止內存泄漏。如果派生類中申請了內存空間,並在其析構函數中對這些內存空間進行釋放。假設基類中采用的是非虛析構函數,當刪除基類指針指向的派生類對象時就不會觸發動態綁定,因而只會調用基類的析構函數,而不會調用派生類的析構函數。那么在這種情況下,派生類中申請的空間就得不到釋放從而產生內存泄漏。所以,為了防止這種情況的發生,C++中基類的析構函數應采用virtual虛析構函數。

虛表的結構:首地址偏移量,虛函數所對應的序號(0開始遞增)。

虛函數的默認參數問題:基類中虛函數的默認參數會在編譯過程就被保存,再調用子類的函數后發生多態,編譯器會使用基類的默認參數;基類有默認參數而子類沒有,則調用的函數永遠是基類中的函數,不能動態綁定的原因是與運行效率有關。

構造函數的類型

  1. 默認構造函數。默認構造函數的原型為 Student(); //沒有參數
  2. 初始化構造函數 Student(int num,int age);//有參數
  3. 復制(拷貝)構造函數 Student(Student&);//形參是本類對象的引用
  4. 轉換構造函數 Student(int r) ;//形參時其他類型變量,且只有一個形參

什么時候會調用析構函數

  • 對象生命周期結束,被銷毀時;
  • delete指向對象的指針時,或delete指向對象的基類類型指針,而其基類虛構函數是虛函數時;
  • 對象i是對象o的成員,o的析構函數被調用時,對象i的析構函數也被調用。

stl的vector和list的區別,hash_table, map增刪分別的時間復雜度

vector:動態數組,擁有一段連續的內存空間。在進行插入和刪除操作時,會造成內存塊的拷貝,時間復雜度為O(n)。當數組中內存空間不夠時,會重新申請一塊內存空間並進行內存拷貝。數組中按照下標隨機訪問的時間復雜度是O(1)。是用數組實現的,每次執行push_back操作,即先free掉原來的存儲,后重新malloc。

list:雙向鏈表,內存空間是不連續的,只能通過指針訪問數據。首尾插入刪除時間復雜度為O(1)。

hash_table: 哈希表,在沒有發生沖突的情況下,查找是O(1),插入刪除O(1)。

map: 底層紅黑樹,刪除插入查找都是O(log n)。紅黑樹的內存比哈希表的內存占用少,map僅需要為其存在的節點分配內存,而Hash事先應該分配足夠的內存存儲散列表,即使有些槽可能棄用.

vector內容的復制:

  1. 初始化構造時拷貝: vector<int> tem(list);
  2. assign: temlist.assign(list.begin(), list.end()); // copy the data, list unchanged
  3. swap: temlist.swap(list); // move list into temlist, list is empty
  4. insert: temlist.insert(temlist.end(), temlist2.begin(), temlist2.end()) // insert temlist2 at the end of temlist

assert()的作用

計算assert里的表達式 expression ,如果其值為假(即為0),那么它先向stderr打印一條出錯信息,然后通過調用 abort 來終止程序運行。 

函數調用過程中的棧幀結構及其變化

被調用函數運行:

push %ebp // %epb寄存器保存的是調用者棧幀的棧底地址,將調用者棧幀的棧底地址壓入棧,即保存舊的%ebp

mov %esp %ebp // 調用者棧幀的棧底%ebp 現在作為了新的棧幀的棧頂 %esp, 為了函數返回時,恢復父函數的棧幀結構

sub $0x16 %esp // 將%esp低地址移動16個字節。有了這么多的儲存空間,才能支持函數里面的各種操作

C中函數的參數是從右到左:為了支持可變長參數形式。若順序是從左到右,除非知道參數個數,否則是無法通過棧指針的相對位移求得最左邊的參數。這樣就變成了左邊參數的個數不確定,正好和動態參數個數的方向相反

一個函數帶有多個參數的時,C++語言沒有規定函數調用時實參的求值順序。這個是編譯器自己規定的。

函數的返回:

movl %ebp %esp  // 使 %esp 和 %ebp 指向同一位置,即子棧幀的起始處, 旨在恢復原來棧頂的狀態

popl  %ebp // 將棧中保存的父棧幀的 %ebp 的值賦值給 %ebp,恢復調用者棧幀的棧底

return 的值是通過%eax寄存器傳遞的

堆棧溢出一般是由什么原因導致的

  • 函數調用層次太深
  • 動態申請空間使用之后沒有釋放
  • 數組訪問越界
  • 指針非法訪問

C++與C#的區別

  • C++是一門面向對象的語言,而C#被認為是一門面向組件(component)的編程語言。面向對象編程聚焦於將多個類結合起來鏈接為一個可執行的二進制程序,而面向組件編程使用可交換的代碼模塊(可獨立運行)並且你不需要知道它們內部是如何工作的就可以使用它們。
  • C++將代碼編譯成機器碼,而C#將代碼編譯成CLR (common language runtime)
  • C++要求用戶手動處理內存,C#有自動垃圾收集機制,防止內存泄露
  • C#不使用指針(pointer),而C++可以在任何時候使用指針。
  • C#中所有對象都只能通過關鍵詞“new”來創建
  • 數組變為了類,因此對於數組里的元素,.NET Framework提供了一系列的操作:查找、排序、倒置
  • 在異常處理上,C++允許拋出任何類型,而C#中規定拋出類型為一個派生於System.Exception的對象。

字節對齊

訪問特定類型的變量的時候經常在特定的內存地址訪問,這就需要各種類型的數據按照一定規則在空間上排列,而不是順序地一個接一個地排放,這種所謂的規則就是字節對齊。這么長一段話的意思是說:字節對齊可以提升存取效率,也就是用空間換時間。

類之間的關系有哪些

關聯:單向,雙向,多元

聚合:空心菱形,兩個類之間有整體和局部的關系,並且就算沒有了整體,局部也可以單獨存在。就像卡車與引擎的關系,離開了卡車,引擎還是能單獨存在。

組合:實心菱形,兩個類之間有整體和局部的關系,部分脫離了整體便不復存在。就像大雁與翅膀的關系一樣

依賴:虛線箭頭,司機這個類,必須要依靠一個車對象才能發揮作用

繼承:空心三角,有多個類出現相同部分的實例變量和方法用繼承,人類與學生類或者老師類都是繼承關系

實現:空心三角虛線,類與接口的關系

C++多態原理

多態定義:程序運行時,父類指針可以根據具體指向的子類對象,來執行不同的函數,表現為多態

原理:

  • 當類中存在虛函數時,編譯器會在類中自動生成一個虛函數表
  • 虛函數表是一個存儲類成員函數指針的數據結構,由編譯器自動生成和維護
  • virtual 修飾的成員函數會被編譯器放入虛函數表中
  • 存在虛函數時,編譯器會為對象自動生成一個指向虛函數表的指針(通常稱之為 vptr 指針)

靜態多態(編譯器多態):函數重載和模板。函數重載,就是具有相同的函數名但是有不同參數列表,模板是c++泛型編程的一大利器

動態多態(運行期多態):類繼承,虛函數

深拷貝和淺拷貝

淺拷貝:只復制指針內容,不復制指針所指對象,結果為兩個指針指向同一塊內存;淺拷貝發生時,通常表明存在着一個“相識關系”。share_ptr
深拷貝:重新為指針分配內存,並將原來指針所指對象的內容拷貝過來,最后結果為兩個指針指向兩塊不同的內存;當深拷貝發生時,通常表明存在着一個“聚合關系”,copy_ptr

4種面向對象的設計原則

  • 單一職責原則 (The Single Responsiblity Principle,簡稱SRP):一個類,最好只做一件事,只有一個引起它的變化.
  • 開放-封閉原則 (The Open-Close Principle,簡稱OCP):對於擴展是開放的,對於更改是封閉的
  • Liskov 替換原則(The Liskov Substitution Principle,簡稱LSP):子類必須能夠替換其基類
  • 依賴倒置原則(The Dependency Inversion Pricinple,簡稱DIP):依賴於抽象
  • 接口隔離原則 (The Interface Segregation Principle,簡稱ISP):使用多個小的專門的接口,而不要使用一個大的總接口

windows下 一個程序的最大內存

  • 在windows 32位操作系統中,每一個進程能使用到的最大空間(包含操作系統使用的內核模式地址空間)為4GB(2的32次方) , 在通常情況下操作系統會分配2GB內存給程序使用,另外2GB內存為操作系統保留
  • 32位的Linux默認占用4GB中的1GB,程序只能使用剩下的3GB
  • 64位的Windows默認占用256TB中的248TB,程序只能使用剩下的8TB。
  • 64位的Linux默認占用256TB中的128TB,程序只能使用剩下的128TB。

c++遇到的一個最深刻的bug

static的作用

  1. 隱藏:變量和函數,如果加了static修飾,就會其它源文件隱藏。利用這一特性可以在不同的文件中定義同名函數和同名變量,而不必擔心命名沖突。Static可以作為函數和變量的前綴,對於函數來講,static的作用僅僅限於隱藏。
  2. 保持變量內容的持久:存儲在靜態數據區的變量會在程序剛開始運行時就完成初始化,也就是唯一的一次初始化。共有兩種變量存儲在靜態存儲區:全局變量和static變量,只不過和全局變量比起來,static可以控制變量的可見范圍。
  3. 默認初始化為0:其實全局變量也具備這一屬性,因為全局變量也存儲在靜態數據區。在靜態數據區,內存中所有的字節默認值都是0x00,某些時候這一特點可以減少程序員的工作量。

內存五大區

  1. :用來存儲一些局部變量以及函數的參數,局部變量等,在函數完成執行,系統自行釋放棧區內存,不需要用戶管理。棧區的大小由編譯器決定,效率比較高,但空間有限。VS中默認的棧區大小為1M
  2. :由程序員手動申請空間,在程序運行期間均有效。堆區的變量需要手動釋放。使用malloc或者new進行堆的申請,堆的總大小為機器的虛擬內存的大小
  3. 全局/靜態存儲區:存儲程序的靜態變量以及全局變量(在程序編譯階段已經分配好內存空間並初始化),整個程序的生命周期都存在的。
  4. 常量存儲區:存放常量字符串的存儲區,只能讀不能寫,const修飾的局部變量存儲在常量區。
  5. 代碼區:存放源程序二進制代碼。

指針和引用

  • 指針是一個變量,這個變量存儲的是一個地址,指向內存的一個存儲單元;引用跟原來的變量實質上是同一個東西,只不過是原變量的一個別名而已。
  • 指針可以有多級,但是引用只能是一級
  • 指針的值可以為空,但是引用的值不能為NULL,並且引用在定義的時候必須初始化
  • 指針的值在初始化后可以改變,即指向其它的存儲單元,而引用在進行初始化后就不會再改變了
  • sizeof引用"得到的是所指向的變量(對象)的大小,而"sizeof指針"得到的是指針本身的大小
  • 指針和引用的自增(++)運算意義不一樣

死鎖

定義:指兩個或兩個以上的進程(線程)在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程(線程)稱為死鎖進程(線程)

必要條件:

  1. 互斥條件:線程/進程對於所分配到的資源具有排它性,即一個資源只能被一個線程/進程占用,直到被該線程/進程釋放
  2. 請求與保持條件:一個線程/進程因請求被占用資源而發生阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:線程/進程已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢后才釋放資源。
  4. 循環等待條件:當發生死鎖時,所等待的線程/進程必定會形成一個環路造成永久阻塞

避免死鎖:破壞產生死鎖的四個條件中的其中一個就可以

  1. 請求與保持條件:一次性申請所有的資源
  2. 不剝奪條件:占用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它占有的資源
  3. 循環等待條件:靠按序申請資源來預防

線程和進程的區別 

  1. 進程是資源分配最小單位,線程是程序執行的最小單位;
  2. 進程有自己獨立的地址空間,每啟動一個進程,系統都會為其分配地址空間,建立數據表來維護代碼段、堆棧段和數據段,線程沒有獨立的地址空間,它使用相同的地址空間共享數據;
  3. CPU切換一個線程比切換進程花費小;
  4. 創建一個線程比進程開銷小;
  5. 線程占用的資源要⽐進程少很多。
  6. 線程之間通信更方便,同一個進程下,線程共享全局變量,靜態變量等數據,進程之間的通信需要以通信的方式(IPC)進行;
  7. 多進程程序更安全,生命力更強,一個進程死掉不會對另一個進程造成影響(源於有獨立的地址空間),多線程程序更不易維護,一個線程死掉,整個進程就死掉了(因為共享地址空間);
  8. 進程對資源保護要求高,開銷大,效率相對較低,線程資源保護要求不高,但開銷小,效率高,可頻繁切換

賽馬問題

參考:https://zhuanlan.zhihu.com/p/103572219

TCP v.s. UDP

  1. 基於連接與無連接;
  2. 對系統資源的要求(TCP較多,UDP少);
  3. UDP程序結構較簡單;
  4. 流模式與數據報模式 ;
  5. TCP保證數據正確性,UDP可能丟包;
  6. TCP保證數據順序,UDP不保證。

如何保證UDP的可靠性

在應用層模仿傳輸層TCP的可靠性傳輸: 發送端發送數據時,生成一個隨機seq=x,然后每一片按照數據大小分配seq。數據到達接收端后接收端放入緩存,並發送一個ack=x的包,表示對方已經收到了數據。發送端收到了ack包后,刪除緩沖區對應的數據。時間到后,定時任務檢查是否需要重傳數據。

  1. 添加seq/ack機制,確保數據發送到對端
  2. 添加發送和接收緩沖區,主要是用戶超時重傳。
  3. 添加超時重傳機制

擁塞避免算法的具體過程

慢啟動:慢啟動為發送方的TCP增加了一個窗口: 擁塞窗口 (cwnd),初始化之后慢慢增加這個cwnd的值來提升速度。同時也引入了ssthresh門限值,如果cwnd達到這個值會讓cwnd的增長變得平滑

  1. 連接建好的開始先初始化cwnd = 1,表明可以傳一個MSS大小的數據
  2. 每當收到一個ACK,cwnd++; 呈線性上升
  3. 每當過了一個RTT,cwnd = cwnd*2; 呈指數讓升
  4. 當cwnd >= ssthresh時,就會進入“擁塞避免算法”

擁堵避免:讓擁塞窗口cwnd緩慢地增大,即每經過一個往返時間RTT就把發送方的擁塞窗口cwnd加1,而不是加倍。這樣擁塞窗口cwnd按線性規律緩慢增長,比慢開始算法的擁塞窗口增長速率緩慢得多;只要發送方判斷網絡出現擁塞(其根據就是沒有收到確認),就要把慢開始門限ssthresh設置為出現擁塞時的發送方窗口值的一半(但不能小於2)。然后把擁塞窗口cwnd重新設置為1,執行慢開始算法。

快速重傳:要求接收方在收到一個失序的報文段后就立即發出重復確認(為的是使發送方及早知道有報文段沒有到達對方)而不要等到自己發送數據時捎帶確認。發送方只要一連收到三個重復確認就應當立即重傳對方尚未收到的報文段,而不必繼續等待設置的重傳計時器時間到期。

快速恢復:當發送方連續收到三個重復確認時,由於發送方現在認為網絡很可能發生擁塞,因此與慢開始不同之處是現在不執行慢開始算法(即擁塞窗口cwnd現在不設置為1),而是把cwnd值設置為慢開始門限ssthresh減半后的數值,然后開始執行擁塞避免算法(“加法增大”),使擁塞窗口緩慢地線性增大。

vector的擴容機制

  • vector通過一個連續的數組存放元素,如果集合已滿,在新增數據的時候,就要分配一塊更大的內存,將原來的數據復制過來,釋放之前的內存,在插入新增的元素
  • 對vector的任何操作,一旦引起空間重新配置,指向原vector的所有迭代器都會失效
  • 初始時刻vector的capacity為0,塞入第一個元素后capacity增加為1
  • 不同的編譯器實現的擴容方式不一樣,VS2015中以1.5倍擴容,GCC以2倍擴容

https和http的區別

http: 一個客戶端和服務器端請求和應答的標准(TCP),用於從WWW服務器傳輸超文本到本地瀏覽器的傳輸協議,它可以使瀏覽器更加高效,使網絡傳輸減少。

https: 以安全為目標的HTTP通道,即HTTP下加入安全基礎SSL層。可以建立一個信息安全通道,來保證數據傳輸的安全和確認網站的真實性

  • https協議需要到ca申請證書
  • http是超文本傳輸協議,信息是明文傳輸,https則是具有安全性的ssl加密傳輸協議。
  • http和https使用的是完全不同的連接方式,用的端口也不一樣,前者是80,后者是443。
  • http的連接很簡單,是無狀態的;HTTPS協議是由SSL+HTTP協議構建的可進行加密傳輸、身份認證的網絡協議,比http協議安全。

如何防止一個類被拷貝

1. delete禁止使用

class noncopyable  
{  
protected:  
    //constexpr noncopyable() = default;  
   // ~noncopyable() = default;  
    noncopyable( const noncopyable& ) = delete;  
    noncopyable& operator=( const noncopyable& ) = delete;  
};

2. 把拷貝構造函數和賦值函數定義為私有函數

const修飾成員函數有什么作用

大根堆建堆的時間復雜度:O(n)

2個鏈表如何判斷是否相交

  • 暴力解法:若第一個鏈表遍歷結束后,還未找到相同的節點,即不存在交點
  • 入棧解法:將鏈表壓棧,通過top判斷棧頂的節點是否相等即可判斷兩個單鏈表是否相交。
  • 遍歷鏈表:同時遍歷兩個鏈表到尾部,同時記錄兩個鏈表的長度。若兩個鏈表最后的一個節點相同,則兩個鏈表相交。設較長的鏈表長度為len1,短的鏈表長度為len2,則先讓較長的鏈表向后移動(len1-len2)個長度。然后開始從當前位置同時遍歷兩個鏈表,當遍歷到的鏈表的節點相同時,則這個節點就是第一個相交的節點。

重載和重寫的區別

重載:就是函數或者方法又相同的名稱,但是參數列表不相同的情形,這樣的同名不同參數的函數或者方法之間相互稱之為重載函數或者方法。
重寫:又稱為方法覆蓋,子類可以繼承父類的方法,而不需要重新編寫相同的方法。但是有時候子類並不想原封不動的繼承父類的方法而是做了一個修改,需要重寫。

sizeof 和 strlen 的區別

  1. sizeof是一個操作符,而strlen是庫函數。
  2. sizeof的參數可以是數據的類型,也可以是變量,而strlen只能以結尾為'\0'的字符串作參數。
  3. 編譯器在編譯時就計算出了sizeof的結果,而strlen必須在運行時才能計算出來。
  4. sizeof計算數據類型占內存的大小,strlen計算字符串實際長度。

對中斷函數的了解

定義:中斷就是在計算機執行程序的過程中,由於出現了某些特殊事情,使得CPU暫停對程序的執行,轉而去執行處理這一事件的程序。等這些特殊事情處理完之后再回去執行之前的程序。

類型:

  • 計算機硬件異常或故障引起的中斷,稱為內部異常中斷
  • 由程序中執行了引起中斷的指令而造成的中斷,稱為軟中斷(這也是和我們將要說明的系統調用相關的中斷);
  • 由外部設備請求引起的中斷,稱為外部中斷

優先級:機器錯誤 > 時鍾 > 磁盤 > 網絡設備 > 終端 > 軟件中斷

內存池、進程池、線程池。(c++程序員必須掌握)

首先介紹一個概念“池化技術 ”。池化技術就是:提前保存大量的資源,以備不時之需以及重復使用。池化技術應用廣泛,如內存池,線程池,連接池等等。內存池相關的內容,建議看看Apache、Nginx等開源web服務器的內存池實現。由於在實際應用當做,分配內存、創建進程、線程都會設計到一些系統調用,系統調用需要導致程序從用戶態切換到內核態,是非常耗時的操作。因此,當程序中需要頻繁的進行內存申請釋放,進程、線程創建銷毀等操作時,通常會使用內存池、進程池、線程池技術來提升程序的性能。

線程池:線程池的原理很簡單,類似於操作系統中的緩沖區的概念,它的流程如下:先啟動若干數量的線程,並讓這些線程都處於睡眠狀態,當需要一個開辟一個線程去做具體的工作時,就會喚醒線程池中的某一個睡眠線程,讓它去做具體工作,當工作完成后,線程又處於睡眠狀態,而不是將線程銷毀。

進程池與線程池同理。

內存池:內存池是指程序預先從操作系統申請一塊足夠大內存,此后,當程序中需要申請內存的時候,不是直接向操作系統申請,而是直接從內存池中獲取;同理,當程序釋放內存的時候,並不真正將內存返回給操作系統,而是返回內存池。當程序退出(或者特定時間)時,內存池才將之前申請的內存真正釋放。

一個程序從開始運行到結束的完整過程(四個過程)

  • 預處理:條件編譯,頭文件包含,宏替換的處理,生成.i文件。
  • 編譯:將預處理后的文件轉換成匯編語言,生成.s文件
  • 匯編:匯編變為目標代碼(機器代碼)生成.o的文件
  • 鏈接:連接目標代碼,生成可執行程序

進程的常見狀態?以及各種狀態之間的轉換條件?

  • 就緒:進程已處於准備好運行的狀態,即進程已分配到除CPU外的所有必要資源后,只要再獲得CPU,便可立即執行。
  • 執行:進程已經獲得CPU,程序正在執行狀態。
  • 阻塞:正在執行的進程由於發生某事件(如I/O請求、申請緩沖區失敗等)暫時無法繼續執行的狀態。

參考資料

  1. https://www.cnblogs.com/tenosdoit/p/3456704.html
  2. https://refactoringguru.cn/design-patterns/bridge
  3. https://www.jianshu.com/p/9d60e5f9cd7e
  4. https://www.cnblogs.com/dolphin0520/archive/2011/04/03/2004869.html
  5. https://www.jianshu.com/p/6c73a4585eba
  6. https://www.jianshu.com/p/e1ce19da9b82

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM