成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

萬字長文超全C++面經

開發
為了方便查閱, 補充了可能沒有面試內容的一級標題. 這樣一級標題可以和 C++ Primer 書籍保持一致.

本文經自動駕駛之心公眾號授權轉載,轉載請聯系出處。

1. 開始

本文目的是整理面試常見的會問到的題目, 具體細節的學習需要參考 C++ Primer / Effective C++ 系列書籍 / Inside the C++ Object Model 進行學習.

為了方便查閱, 補充了可能沒有面試內容的一級標題. 這樣一級標題可以和 C++ Primer 書籍保持一致.

1.1. C 和 C++ 的區別

設計思想上:

  • C++ 是面向對象的語言, C 是面向過程的語言

語法上:

  • C++ 具有封裝/繼承/多態三種特性.
  • C++ 相比 C, 增加了類型安全的功能, 比如強制類型轉換.
  • C++ 支持范式編程, 比如模板類/函數模板等.

2. 變量和基本類型

2.1. 復合類型

復合類型(compound type)是指基于其他類型定義的類型. 最常見的是引用和指針.

引用即別名: 引用(reference)為對象起了另外一個名字, 引用類型引用(refers to)另外一種類型.

  • 定義引用時, 程序把引用和它的初始值綁定在一起, 而不是將初始值拷貝給引用. 一旦初始化完成, 引用將和它的初始值對象一直綁定在一起. 因為無法令引用重新綁定到另外一個對象, 因此引用必須初始化.
  • 因為引用不是一個對象, 所以不能定義引用的引用.

指針(pointer)是指向(point to)另外一種類型的復合類型.

  • 指針無需在定義時賦初值.
  • 指針本身就是一個對象, 允許對指針賦值和拷貝, 而且在指針的生命周期內它可以先后指向幾個不同的對象.

表 2.1 指針與數組的區別

2.2. const限定符

2.2.1. 作用

  • 修飾變量: 表明該變量的值不可以被改變.
  • 修飾指針: 區分指向常量的指針和常量指針.
  • 修飾引用: 用于形參, 既避免了拷貝, 又避免了函數對值的修改.
  • 修飾成員函數: 表示函數不能修改成員變量(實際上是修飾this指針)

補充:

  • 對于局部對象,常量存放在棧區;
  • 對于全局對象, 常量存放在全局/靜態存儲區;
  • 對于字面值常量, 常量存放在常量存儲區(代碼段).

2.2.2. 指向常量的指針 VS 常量指針

參考 C++ Primer 2.4.2 指針和const:

  • 指向常量的指針(pointer to const):
  • 具有只能夠讀取內存中數據, 卻不能夠修改內存中數據的屬性的指針(底層 const).
  • const int * p;或者int const * p;
  • 常量指針(const pointer): 常量指針是指指針所指向的位置不能改變, 即指針本身是一個常量(頂層 const), 但是指針所指向的內容可以改變.
  • 常量指針必須在聲明的同時對其初始化, 不允許先聲明一個指針常量隨后再對其賦值, 這和聲明一般的常量是一樣的.
  • int * const p = &a;

2.2.3. cosntexpr

  • 常量表達式(const expression)是指值不會改變并且在編譯過程就能得到計算結果的表達式.
  • 一般來說, 如果認定變量是一個常量表達式, 那就把它聲明成constexpr類型.
  • 一個constexpr指針的初始值必須是nullptr或者0, 或者是存儲于某個固定地址中的對象.
  • constexpr函數是指能用于常量表達式的函數.
  • 函數的返回類型及所有的形參的類型都得是字面值類型.
  • 函數體中必須有且只有一條return語句.

2.2.4. #define VS const

3. 字符串、向量和數組

4. 表達式

4.1. 右值

C++的表達式要不然是右值(rvalue), 要不然是左值(lvalue). 這兩個名詞是從 C 語言繼承過來的, 原本是為了幫助記憶: 左值可以位于賦值語句的左側, 右值則不能.

當一個對象被用做右值的時候, 用的是對象的值(內容); 當對象被用做左值的時候, 用的是對象的身份(在內存中的位置).

4.2. ++i/i++

前置版本++i: 首先將運算對象加 1, 然后將改變后的對象作為求值結果.

后置版本i++: 也會將運算對象加 1, 但是求解結果是運算對象改變之前的那個值的副本.

以下摘錄自 More Effective C++ Item 6:

// prefix form(++i): increment and fetch
UPInt&  UPInt::operator++()
{
    *this +=1;        // increment
    return *this;     // fetch
}
// postfix form(i++): fetch and increment
const UPInt UPInt::operator++(int)
{
    const UpInt oldValue = *this; // fetch
    ++(*this);                    // increment
    return oldValue;             // return what was fetched
}

4.3. sizeof運算符

4.3.1. 普通變量執行sizeof

sizeof運算符的結果部分地依賴于其作用的類型:

  • 對char或者類型為char的表達式執行sizeof運算, 結果得 1.
  • 對引用類型執行sizeof運算得到被引用對象所占空間的大小.
  • 對指針執行sizeof運算得到指針本身所占空間的大小.
  • 對解引用指針執行sizeof運算得到指針指向的對象所占空間的大小.
  • 對數組執行sizeof運算得到整個數組所占空間的大小, 等價于對數組中所有元素各執行一次sizeof運算并將所得結果求和.
  • 對string對象或vector對象執行sizeof運算只返回該類型固定部分的大小.

4.3.2. 類執行sizeof

class A {};
class B { B(); ~B() {} };
class C { C(); virtual ~C() {} };
class D { D(); ~D() {} int d; };
class E { E(); ~E() {} static int e; };
int main(int argc, char* argv[]) {
    std::cout << sizeof(A) << std::endl; // 輸出結果為1
    std::cout << sizeof(B) << std::endl; // 輸出結果為1
    std::cout << sizeof(C) << std::endl; // 輸出結果為8,實例中有一個指向虛函數表的指針
    std::cout << sizeof(D) << std::endl; // 輸出結果為4,int占4個字節
    std::cout << sizeof(E) << std::endl; // 輸出結果為1,static不算
    return 0;
}
  • 定義一個空類型, 里面沒有成員變量和成員函數, 求sizeof結果為 1. 空類型的實例中不包括任何信息, 本來求sizeof得到0, 但是當我們聲明該類型的實例的時候, 它必須在內存中占有一定的空間, 否則則無法使用這些實例, 至于占用多少內存, 由編譯器決定, 一般有一個char類新的內存.
  • 如果在該類型中添加一個構造函數和析構函數, 再對該類型求sizeof結果仍為 1. 調用構造函數和析構函數只需要知道函數的地址即可, 而這些函數的類型只與類型相關, 而與類型的實例無關, 編譯器也不會因為這兩個函數在實例內添加任何額外的信息.
  • 如果把析構函數標記為虛函數, 就會為該類型生成虛函數表, 并在該類型的每一個實例中添加一個指向虛函數表的指針. 在 32 位的機器上, 一個指針占 4 字節的空間, 因此求sizeof得到 4; 在 64 位機器上, 一個指針占 8 字節的空間, 因此求sizeof得到 8.

4.4. 顯式轉換

  • static_cast: 任何具有明確定義的類型轉換, 只要不包含底層const, 都可以使用static_cast.
  • dynamic_cast: 用于(動態)多態類型轉換. 只能用于含有虛函數的類, 用于類層次間的向上向下轉化.
  • const_cast: 去除"指向常量的指針"的const性質.
  • reinterpret_cast: 為運算對象的位模式提供較低層次的重新解釋, 常用于函數指針的轉換.

5. 語句

6. 函數

6.1. 函數基礎

6.1.1. 形參和實參

實參是形參的初始值.

6.1.2. static

  • 修飾局部變量: 使得被修飾的變量成為靜態變量, 存儲在靜態區. 存儲在靜態區的數據生命周期與程序相同, 在main函數之前初始化, 在程序退出時銷毀. 默認初始化為 0.
  • 修飾全局變量: 限制了鏈接屬性, 使得全局變量只能在聲明它的源文件中訪問.
  • 修飾普通函數: 使得函數只能在聲明它的源文件中訪問.
  • 修飾類的成員變量和成員函數: 使其只屬于類而不是屬于某個對象. 對多個對象來說, 靜態數據成員只存儲一處, 供所有對象共用.
  • 靜態成員調用格式<類名>::<靜態成員>
  • 靜態成員函數調用格式<類名>::<靜態成員函數名>(<參數表>)

6.2. 參數傳遞

指針參數傳遞本質上是值傳遞, 它所傳遞的是一個地址值.

一般情況下, 輸入用傳值或者傳const reference. 輸出傳引用(或者指針).

6.3. 內聯函數

6.3.1. 使用

將函數指定為內聯函數(inline), 通常就是將它在每個調用點上"內聯地"展開.

一般來說, 內聯機制用于優化規模較小(Google C++ Style 建議 10 行以下)、流程直接、頻繁調用的函數.

在類聲明中定義的函數, 除了虛函數的其他函數都會自動隱式地當成內聯函數.

6.3.2. 編譯器對inline函數的處理步驟

  • 將inline函數體復制到inline函數調用點處;
  • 為所用inline函數中的局部變量分配內存空間;
  • 將inline函數的的輸入參數和返回值映射到調用方法的局部變量空間中;
  • 如果inline函數有多個返回點, 將其轉變為inline函數代碼塊末尾的分支(使用 GOTO).

6.3.3. 優缺點

優點:

  1. 內聯函數同宏函數一樣將在被調用處進行代碼展開, 省去了參數壓棧、棧幀開辟與回收, 結果返回等, 從而提高程序運行速度.
  2. 內聯函數相比宏函數來說, 在代碼展開時, 會做安全檢查或自動類型轉換(同普通函數), 而宏定義則不會.
  3. 在類中聲明同時定義的成員函數, 自動轉化為內聯函數, 因此內聯函數可以訪問類的成員變量, 宏定義則不能.
  4. 內聯函數在運行時可調試, 而宏定義不可以.

缺點:

  1. 代碼膨脹. 內聯是以代碼膨脹(復制)為代價, 消除函數調用帶來的開銷. 如果執行函數體內代碼的時間, 相比于函數調用的開銷較大, 那么效率的收獲會很少. 另一方面, 每一處內聯函數的調用都要復制代碼, 將使程序的總代碼量增大, 消耗更多的內存空間.
  2. inline函數無法隨著函數庫升級而升級. inline函數的改變需要重新編譯, 不像non-inline可以直接鏈接.
  3. 是否內聯, 程序員不可控. 內聯函數只是對編譯器的建議, 是否對函數內聯, 決定權在于編譯器.

6.4. 返回類型和return語句

調用一個返回引用的函數得到左值, 其他返回類型得到右值.

6.5. 特殊用途語言特性

6.5.1. 調試幫助

assert是一種預處理器宏. 使用一個表達式作為它的條件:

assert(expr);

首先對expr求值, 如果表達式為false. assert輸出信息并終止程序的執行. 如果表達式為true. assert什么也不做.

6.6. 函數指針

函數指針指向的是函數而非對象. 和其他指針一樣, 函數指針指向某種特定類型. 函數的類型由它的返回類新和形參共同決定, 與函數名無關.

C 在編譯時, 每一個函數都有一個入口地址, 該入口地址就是函數指針所指向的地址.

有了指向函數的指針變量后,可用該指針變量調用函數,就如同用指針變量可引用其他類型變量一樣

用途: 調用函數和做函數的參數, 比如回調函數.

char * fun(char * p)  {…}  // 函數fun
char * (*pf)(char * p);    // 函數指針pf
pf = fun;                  // 函數指針pf指向函數fun
pf(p);                     // 通過函數指針pf調用函數fun

7. 類

7.1. 定義抽象數據類型

7.1.1. this指針

  • this指針是一個隱含于每一個非靜態成員函數中的特殊指針. 它指向調用該成員函數的那個對象.
  • this的目的總是指向"這個"對象, 所以this是一個常量指針, 被隱含地聲明為:ClassName * const this, 這意味著不能給this指針賦值;
  • 在ClassName類的const成員函數中, this指針的類型為:const ClassName* const, 這說明不能對this指針所指向對象進行修改.
  • 當對一個對象調用成員函數時, 編譯程序先將對象的地址賦給this指針, 然后調用成員函數, 每次成員函數存取數據成員時, 都隱式使用this指針.
  • 當一個成員函數被調用時, 自動向它傳遞一個隱含的參數, 該參數是一個指向這個成員函數所在的對象的指針.
  • this并不是一個常規變量, 而是個右值, 所以不能取得this的地址(不能&this).
  • 在以下場景中, 經常需要顯式引用this指針:
  • 為實現對象的鏈式引用;
  • 為避免對同一對象進行賦值操作;
  • 在實現一些數據結構時, 如list.

7.1.2. 拷貝函數

  • C++深拷貝與淺拷貝
  • 在未定義顯示拷貝構造函數的情況下, 系統會調用默認的拷貝函數——即淺拷貝, 它能夠完成成員的一一復制. 當數據成員中沒有指針時, 淺拷貝是可行的; 但當數據成員中有指針時, 如果采用簡單的淺拷貝, 則兩類中的兩個指針將指向同一個地址, 當對象快結束時, 會調用兩次析構函數, 而導致指針懸掛現象, 所以此時必須采用深拷貝.
  • 深拷貝與淺拷貝的區別就在于深拷貝會在堆內存中另外申請空間來儲存數據, 從而也就解決了指針懸掛的問題. 簡而言之, 當數據成員中有指針時, 必須要用深拷貝.

7.1.3. 析構函數

(TODO: 整理析構函數的特性)

  • 析構順序與構造函數的構造順序相反.
  • 當對象結束生命周期時, 系統會自動執行析構函數.
  • 析構函數聲明時在函數名前加取反符~, 不帶任何參數, 也沒有返回值.
  • 如果用戶沒有聲明析構函數, 系統會自動生成一個缺省的析構函數.
  • 如果類中有指針, 且在使用的過程中動態申請了內存, 那么需要顯示構造析構函數, 在銷毀類之前, 釋放掉申請的內存空間, 避免內存泄漏.

7.2. 訪問控制與封裝

7.2.1. public/private/protected

  • 定義在public說明符之后的成員在整個程序內可被訪問, public成員定內的接口.
  • 定義在private說明符之后的成員可以被類的成員函數訪問, 但是不能被使用該類的代碼訪問, private部分封裝了(即隱藏了)類的實現細節.
  • 基類希望它的派生類有權訪問該成員, 同時禁止其他用戶訪問. 我們用受保護的(protected)訪問運算符說明這樣的成員.

7.2.2. struct和class的區別

  • struct與class定義的唯一區別就是默認的訪問權限(struct默認是public, class默認是private).
  • 使用習慣上, 只有少量成員變量的的用struct定義.

7.2.3. 友元

類可以允許其他類或者函數訪問它的非公有成員, 方法是令其他類或者函數成為它的有元(friend).

7.3. 構造函數再探

7.3.1. 初始化順序

成員變量的初始化順序與它們在類定義中的出現順序一致: 構造函數初始值列表中初始值的前后位置關系不會影響

7.3.2. explicit

  • 用于類的構造函數, 阻止其執行隱式類型轉換, 但是仍可以被用來進行顯式類型轉換.

8. I/O 庫

9. 順序容器

9.1. 容器庫概覽

9.1.1. 迭代器

  • 迭代器(Iterator)模式又稱游標(Cursor)模式, 用于提供一種方法順序訪問一個聚合對象中各個元素, 而又不需暴露該對象的內部表示.
  • 迭代器本質上是類模板, 只是表現地像指針.

9.2. 順序容器操作

9.2.1. emplace

當調用push或insert成員函數時, 我們將元素類型的對象傳遞給它們, 這些對象被拷貝到容器中. 而當我們調用一個emplace成員函數時, 則是將參數傳遞給元素類型的構造函數. emplace成員使用這些參數在容器管理的內存空間中直接構造元素.

9.2.2. resize/reserve

  • resize: 改變容器內含有元素的數量.
  • reserve: 改變容器的最大容量.

9.2.3. 容器操作可能使迭代器失效

在向容器中添加元素后:

  • 如果容器是vector或string, 且存儲空間被重新分配, 則指向容器的迭代器, 指針和引用都會失效.
  • 對于deque, 插入到除首尾位置之外的任何位置都會導致迭代器指針和引用失效.
  • 對于list, 指向容器的迭代器指針和引用仍然有效.

從容器刪除元素后:

  • 對于list, 指向容器的迭代器指針和引用仍然有效.
  • 對于deque, 在首尾之外的任何位置刪除元素, 其他元素的迭代器也會失效.
  • 對于vector或string, 被刪元素之前的迭代器仍有效, 尾后迭代器失效.
  • 對于關聯式容器(如std::set / std::map), 插入元素不會使任何迭代器失效.
  • 對于無序關聯式容器(如std::unordered_set / std::unordered_map), 插入元素之后如果發生了 Rehash(新元素的個數大于max_load_factor() * bucket_count()), 則所有迭代器將失效.

9.3. vector

//動態申請數組
const int M = 10;
const int N = 10;

//使用new申請一個一維數組.訪問p_arr[i].
int* p_arr = new int[N];
//使用new申請一個二維數組.訪問:p_arr[i][j].
int(*p_arr)[N] = new int[M][N];
//一維數組轉化為二維數組.訪問:p_arr[i*N+j].
int* p_arr = new int[M*N];
//指向指針的指針(指向一維指針數組).訪問p[i][j]
int** p_arr = new int* [M]
for(int i = 0; i < M; i++)
    p_arr[i] = new int[N];
//回收內存
for(int i = 0; i < M; i++)
 delete []p_arr[i];
delete []p_arr;

//使用vector申請一個一維數組
vector<int> v_arr(n, 0);
vector<int> v_arr{1,0};
//使用vector申請一個二維數組, 如果不初始化, 使用[]會報錯
vector<vector<int>> v_arr(m, vector<int>(n, 0));
vector<vector<int>> v_arr = {{1,0}};

//一維數組作為函數參數
void function(int* a);
void function(int a[]);
void function(int a[N]);
//二維數組作為函數參數,他們合法且等價
void function(int a[M][N]);
void function(int a[][N]);
void function(int (*a)[N])

9.4. string

string s("hello world")
string s2 = s.substring(0, 5); // s2 = hello
string s3 = s.substring(6);    // s3 = world
string s4 = s.substring(6, 11);// s4 = world
string s5 = s.substring(12);   // 拋出一個out_of_range異常

isalpha(ch); //判斷一個字符是否是字母
isalnum(ch); //判斷一個字符是數字或字母
tolower(ch); //將字母轉化成小寫
toupper(ch); //將字母轉化為大寫

string str = to_string(num); //將數字轉換成字符串

9.5. vector對象是如何增長的

當不得不獲取新的內存空間時, vector和string的實現通常會分配一個比新的空間需求更大的內存空間. 容器預留這些空間作為備用, 可以用來保存更多的新元素. 這樣, 就不需要每次添加新元素都重新分配容器的內存空間了.

  • capacity操作告訴我們容器在不擴張內存空間的情況下可以容納多少個元素. reserve操作允許我們通知容器它應該準備保存多少個元素.
  • 初始時刻vector的capacity為 0, 塞入第一個元素后capacity增加為 1.
  • 不同的編譯器實現的擴容方式不一樣, VS2015 中以 1.5 倍擴容, GCC以 2 倍擴容.
  • 從空間上分析, 擴容因子越大, 意味著預留空間越大, 浪費的空間也越多, 所以從空間考慮, 擴容因子因越小越好.
  • 從時間上分析, 如果預留空間不足的話, 就需要重新開辟一段空間, 把原有的數據復制到新空間, 如果擴容因子無限大的話, 那顯然就不再需要額外開辟空間了. 所以時間角度看, 擴容因子越大越好.

9.6. 容器適配器

除了順序容器外, 標準庫還定義了三個順序容器適配器: stack、queue和priority_queue.

本質上, 一個適配器是一種機制, 能使某種事物的行為看起來像另外一種事物一樣.

默認情況下, stack和queue是基于deque實現的, priority_queue是在vector之上實現的.

9.6.1. priority_queue

std::priority_queue<int> q1; // 默認大根堆
std::priority_queue<int, std::vector<int>, std::greater<int>>
    q2(data.begin(), data.end()); // 小根堆
// 使用lambda表達式
auto cmp = [](int left, int right) { return (left ^ 1) < (right ^ 1); };
std::priority_queue<int, std::vector<int>, decltype(cmp)> q3(cmp);

10. 泛型算法

10.1. lambda 表達式

一個 lambda 表達式表示一個可調用的代碼單元. 我們可以將其理解為一個未命名的內聯函數. 一個 lambda 表達式具有如下形式:

[capture list](parameter list) -> return type {function body}

其中capture list(捕獲列表)是一個 lambda 所在函數中定義的局部變量的列表(通常為空); return type, parameter list和function body與任何普通函數一樣, 分別表示返回類型、參數列表和函數體. 但是與普通函數不同, lambda 必須使用尾置返回來制定返回類新.

我們可以忽略參數列表和返回類型, 但必須包含捕獲列表和函數體:

auto f = [] {return 42};

11. 關聯容器

  • map: 關鍵字-值對; set: 關鍵字即值.
  • map: 按關鍵字有序保存元素(底層為紅黑樹); unordered_map: 無序集合(底層為哈系表).
  • map: 關鍵字不可重復出現; multimap: 關鍵字可重復出現.

12. 動態內存

12.1. 智能指針

智能指針的行為類似常規指針, 重要的區別在于它負責自動釋放所指向的對象.

shared_ptr
  • 允許多個指針指向同一個對象.
  • 我們可以認為每個shared_ptr都有一個關聯的計數器, 通常稱其為引用計數. 一旦一個shared_ptr的計數器變為 0, 他就會自動釋放自己所管理的對象.
unique_ptr
  • "獨占"所指向的對象.
weak_ptr
  • weak_ptr是一種弱引用, 指向shared_ptr所管理的對象.
  • 可打破環狀引用(cycles of references, 兩個其實已經沒有被使用的對象彼此相互指向, 使之看似還在 “被使用” 的狀態)的問題.
make_shared
  • make_shared 在動態內存中分配一個對象并初始化它, 返回指向此對象的shared_ptr.

13. 拷貝控制

13.1. 對象移動

  • 右值引用: 所謂右值引用就是必須綁定到右值的引用. 我們通過&&而不是&來獲得右值引用. 右值引用有一個重要的性質: 只能綁定到一個將要銷毀的對象.
  • 左值持久, 右值短暫: 左值有持久的狀態, 而右值要么是字面常量, 要么是在表達式求值過程中創建的臨時對象.
  • 通過調用std::move來獲得綁定到左值上的右值引用.
int &&rr1 = 42;  // 正確: 字面常量是右值
int &&rr2 = rr1; // 錯誤: 表達式rr1是左值
int &&rr3 = std::move(rr1); // ok

14. 重載運算與類型轉換

15. 面向對象程序設計

15.1. OOP: 概述

面向對象程序設計(object-oriented programming)的核心思想是數據抽象(封裝)、繼承和動態綁定(多態).

  • 通過數據抽象, 我們可以將接口與實現分離;
  • 使用繼承, 可以定義相似的類型并對其相似關系建模;
  • 使用動態綁定, 可以在一定程度上忽略相似類型的區別, 而以統一的方式使用它們的對象.

15.2. 定義派生類和基類

15.2.1. 初始化順序

  • 每個類控制它自己的成員初始化過程
  • 首先初始化基類的部分, 然后按照聲明的順序依次初始化派生類的成員.

15.2.2. 靜態多態/動態多態

  • 靜態多態是通過重載和模板技術實現,在編譯的時候確定.
  • 動態多態通過虛函數和繼承關系來實現,執行動態綁定, 在運行的時候確定.
  • 重載: 兩個函數名相同,但是參數的個數或者類型不同.
  • 重寫: 子類繼承父類,符類中函數被聲明為虛函數,子類中重新定義了這個虛函數.

15.3. 虛函數

  • 虛函數: 基類希望派生類覆蓋的函數, 可以將其定義為虛函數, 這樣每一個派生類可以各自定義適合自生的版本.
  • 當基類定義virtual函數的時候, 它希望派生類可以自己定義這個函數.
  • 如果使用virtual, 程序依據引用或者指針所指向對象的類型來選擇方法(method).
  • 如果不使用virtual, 程序依據引用類型或者指針類型選擇一個方法(method).
  • 虛函數表指針: 在有虛函數的類的對象最開始部分是一個虛函數表的指針, 這個指針指向一個虛函數表.
  • 虛函數表中放了虛函數的地址, 實際的虛函數在代碼段(.text)中.
  • 當子類繼承了父類的時候也會繼承其虛函數表, 當子類重寫父類中虛函數時候, 會將其繼承到的虛函數表中的地址替換為重新寫的函數地址.
  • 使用了虛函數, 會增加訪問內存開銷, 降低效率.

15.3.1. 虛析構函數

Q: 析構函數為什么是虛函數?

A: 將可能會被繼承的基類的析構函數設置為虛函數, 可以保證當我們new一個派生類, 然后使用基類指針指向該派生類對象, 基類指針時可以釋放掉派生類的空間, 防止內存泄漏.

Q: 為什么 C++ 默認析構函數不是虛函數?

A: C++默認的析構函數不是虛函數是因為虛函數需要額外的虛函數表和虛表指針, 占用額外的內存; 所以只有當一個類會被用作基類時才將其設置為虛函數.

15.4. 抽象基類

  • 純虛函數是一種特殊的虛函數, 在基類中不能對虛函數給出有意義的實現, 而把它聲明為純虛函數, 它的實現留給該基類的派生類去做. 書寫=0就可以將一個虛函數說明為純虛函數.
  • 含有(或者未經覆蓋直接繼承)純虛函數的類是抽象基類(abstract base class).

虛函數 VS 純虛函數

  • 類里如果聲明了虛函數, 這個函數是實現的, 哪怕是空實現, 它的作用就是為了能讓這個函數在它的子類里面可以被覆蓋(override), 這樣的話, 編譯器就可以使用后期綁定來達到多態了. 純虛函數只是一個接口, 是個函數的聲明而已, 它要留到子類里去實現.
  • 虛函數在子類里面可以不重寫; 但純虛函數必須在子類實現才可以實例化子類.
  • 虛函數的類用于 “實作繼承”, 繼承接口的同時也繼承了父類的實現. 純虛函數關注的是接口的統一性, 實現由子類完成.
  • 帶純虛函數的類叫抽象類, 這種類不能直接生成對象, 而只有被繼承, 并重寫其虛函數后, 才能使用. 抽象類被繼承后, 子類可以繼續是抽象類, 也可以是普通類.

15.5. 訪問控制與繼承

  • 公有繼承保持原始狀態(沒有特殊要求一般用公有繼承)
  • 私有繼承基類的所有成員都作為派生類的私有成員
  • 保護繼承基類的public作為派生類的保護成員, 其他不變.

16. 模板與泛型編程

17. 標準庫特殊實施

18. 用于大型程序的工具

18.1. 多重繼承與虛繼承

  • 虛繼承是解決 C++ 多重繼承問題的一種手段, 從不同途徑繼承來的同一基類, 會在子類中存在多份拷貝, 即浪費存儲空間, 又存在二義性的問題.
  • 底層實現原理與編譯器相關, 一般通過虛基類指針和虛基類表實現, 每個虛繼承的子類都有一個虛基類指針(占用一個指針的存儲空間, 4 字節)和虛基類表(不占用類對象的存儲空間)(需要強調的是, 虛基類依舊會在子類里面存在拷貝, 只是僅僅最多存在一份而已, 并不是不在子類里面了); 當虛繼承的子類被當做父類繼承時, 虛基類指針也會被繼承.
  • 實際上, vbptr 指的是虛基類表指針(virtual base table pointer), 該指針指向了一個虛基類表(virtual table), 虛表中記錄了虛基類與本類的偏移地址; 通過偏移地址, 這樣就找到了虛基類成員, 而虛繼承也不用像普通多繼承那樣維持著公共基類(虛基類)的兩份同樣的拷貝, 節省了存儲空間.

19. 特殊工具和技術

19.1. 控制內存分配

19.1.1. new & delete

string *sp = new string("a value); // 分配并初始化一個string對象
string *arr = new string[10];      // 分配10個默認初始化的string對象

當我們使用一條new表達式時, 實際執行了三步操作:

  • new表達式調用一個名為operate new(或者operate new[])的標準庫函數. 該函數(從自由存儲區上)分配一塊足夠大的, 原始的, 未命名的內存空間(無需指定內存塊的大小)以便存儲特定類型的對象(或對象的數組).
  • 編譯器運行相應的構造函數以構造這些對象, 并為其傳入初值.
  • 對象被分配了空間并構造完成, 返回一個指向該對象的指針.
delete sp;  // 銷毀*sp, 然后釋放sp指向的內存空間
delete [] arr; // 銷毀數組中的元素, 然后釋放對應的內存空間

當我們使用一條delete表達式刪除一個動態分配的對象時, 實際執行了兩步操作:

  1. 對sp所指的對象或者arr所指的數組中的元素執行對應的析構函數.
  2. 編譯器調用名為operate delete(或者operate delete[])的標準庫函數釋放內存空間.

19.1.2. malloc&free

  • malloc需要顯式的指出內存大小: 函數接受一個表示待分配字節數的size_t.
  • 返回指向分配空間的指針(void*)或者返回 0 以表示分配失敗. (從堆上動態分配內存)
  • free函數接受一個void*, 它是malloc返回的指針的副本, free將相關內存返回給系統. 調用free(0)沒有任何意義.
// operate new的一種簡單實現
void *operater new(size_t size) {
    if (void *men = malloc(size))
        return mem;
    else
        throw bad_alloc();
}
// opearte delete的一種簡單實現
void operator delete(void *mem) noexcept { free(mem); }

19.2. 固有的不可移植特性

19.2.1. volatile

  • 當對象的值可能在程序控制或檢測之外(操作系統、硬件、其它線程等)被改變時, 應該將該對象聲名為volatile. 關鍵字volatile告訴編譯器不應對這樣的對象進行優化.
  • volatile關鍵字聲明的變量, 每次訪問時都必須從內存中取出值(沒有被volatile修飾的變量, 可能由于編譯器的優化, 從 CPU 寄存器中取值).

19.2.2. extern

  • 在多個文件之間共享對象.
  • extern "C"的作用是讓 C++ 編譯器將extern "C"聲明的代碼當作 C 語言代碼處理, 可以避免 C++ 因符號修飾導致代碼不能和 C 語言庫中的符號進行鏈接的問題.

20. 鏈接裝載與庫

本小節內容大部分摘錄自《程序員的自我修養 - 鏈接裝載與庫》

20.1. .h 和 .cpp 文件的區別

  • .h文件里面放申明, .cpp文件里面放定義.
  • .cpp文件會被編譯成實際的二進制代碼, 而.h文件是在被 include 中之后復制粘貼到 .cpp 文件里.

20.2. 編譯和鏈接

  1. 預編譯(預處理): 預編譯過程主要處理那些源代碼文件中的以"#"開始的預編譯指令. 比如"#include"、"#define"等. 生成.i或者.ii文件.
  2. 編譯: 把預處理完的文件進行一系列的詞法分析、語法分析、語義分析及優化后生產相應的匯編代碼文件(.s文件).
  3. 匯編: 將匯編代碼轉變成機器可以執行的指令(機器碼), 生成.o文件.
  4. 鏈接: 鏈接器進行地址和空間分配、符號決議、重定位等步驟, 生成 .out文件.

20.3. 程序的內存布局

一般來講, 應用程序使用的內存空間里有如下"默認"區域.

  • 棧: 棧用于維護函數調用的上下文. 由操作系統自動分配釋放, 一般包含以下幾個方面:
  • 函數的返回地址和參數
  • 臨時變量: 包括函數的非靜態局部變量以及編譯器自動生成的其他臨時變量
  • 保存上下文: 包括函數調用前后需要保持不變的寄存器
  • 堆: 堆是用來容納應用程序動態分配的內存區域. 由程序員分配釋放 ,當程序使用malloc或者new分配內存時, 得到的內存來自堆里.
  • 可執行文件映像: 存儲著可執行文件在內存里的映像, 由裝載器在裝載時將可執行文件的內存讀取或映射到這里.
  • .data: 靜態區, 存放全局變量和局部靜態變量.
  • .bss: 存放未初始化的全局變量和局部靜態變量.
  • .text: 代碼區, 存放 C 語言編譯后的機器代碼, 不可在運行期間修改.
  • 保留區: 保留區并不是一個單一的內存區域, 而是對內存中受到保護而禁止訪問的內存區域的總稱. 如通常 C 語言將無效指針賦值為 0(NULL), 因此 0 地址正常情況下不可能有有效的訪問數據.

圖 20.1 Linux 進程地址空間布局

20.3.1. 段錯誤

Q: 程序出現"段錯誤(segment fault)"或者"非法操作, 該內存地址不能 read/wirte"的錯誤信息, 是什么原因?

A: 這是典型的非法指針解引用造成的錯誤. 當指針指向一個不允許讀或寫的內存地址, 而程序卻試圖利用指針來讀或寫該地址的時候, 就會出現這個錯誤. 可能的段錯誤發生的時機如下:

  • 指針沒有初始化或者初始化為nullptr, 之后沒有給它一個合理的值就開始使用指針.
  • 使用野指針(指向一個已刪除的對象或者未申請訪問受限內存區域的指針).
  • 指向常量的指針試圖修改相關內容.

20.4. 編譯型語言 VS 解釋型語言

  • 有的編程語言要求必須提前將所有源代碼一次性轉換成二進制指令, 也就是生成一個可執行程序(Windows 下的 .exe), 比如 C 語言、C++、Golang、Pascal(Delphi)、匯編等, 這種編程語言稱為編譯型語言, 使用的轉換工具稱為編譯器.
  • 有的編程語言可以一邊執行一邊轉換, 需要哪些源代碼就轉換哪些源代碼, 不會生成可執行程序, 比如 Python、JavaScript、PHP、MATLAB 等, 這種編程語言稱為解釋型語言, 使用的轉換工具稱為解釋器.
責任編輯:張燕妮 來源: 自動駕駛之心
相關推薦

2024-05-10 12:59:58

PyTorch人工智能

2021-10-18 11:58:56

負載均衡虛擬機

2022-09-06 08:02:40

死鎖順序鎖輪詢鎖

2021-01-19 05:49:44

DNS協議

2022-09-14 09:01:55

shell可視化

2021-12-10 12:20:06

LinuxCC++

2020-07-15 08:57:40

HTTPSTCP協議

2020-11-16 10:47:14

FreeRTOS應用嵌入式

2020-07-09 07:54:35

ThreadPoolE線程池

2022-10-10 08:35:17

kafka工作機制消息發送

2024-03-07 18:11:39

Golang采集鏈接

2022-07-19 16:03:14

KubernetesLinux

2019-11-06 10:12:19

B端設計流程分析

2024-11-28 08:00:00

2023-06-12 08:49:12

RocketMQ消費邏輯

2021-08-26 05:02:50

分布式設計

2022-09-08 10:14:29

人臉識別算法

2022-07-15 16:31:49

Postman測試

2024-01-05 08:30:26

自動駕駛算法

2022-02-15 18:45:35

Linux進程調度器
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 免费a网 | 亚洲精品一区二区三区蜜桃久 | 搞av.com| 精品一区二区三区免费毛片 | 伊人久久综合影院 | 国产激情在线 | 日韩成人免费 | 久久国产精品久久久久 | 秋霞在线一区 | 国产美女久久久 | 久久久妇女国产精品影视 | 成人区精品一区二区婷婷 | 91日韩在线 | 美女视频. | 免费在线成人 | 波多野结衣一区二区三区 | 成人精品鲁一区一区二区 | 国产一区在线免费 | 精品欧美一区二区在线观看 | 午夜www | 久久国产欧美日韩精品 | 成人欧美一区二区三区色青冈 | 日韩视频在线播放 | 欧美h版 | 精品久久香蕉国产线看观看亚洲 | 日本在线视频一区二区 | 国产一区二区三区欧美 | 一区二区三区国产 | 能看的av | 成人欧美日韩一区二区三区 | 久草免费福利 | 日韩午夜在线播放 | 亚洲精品国产一区 | 精品久久国产 | 国产伦精品一区二区 | 亚洲 欧美 另类 日韩 | 成人午夜电影网 | 国产视频精品视频 | 亚洲成av | 亚洲精品www | 国产精品一区二区无线 |