五萬字長文 C C++ 面試知識總結(上)

列文發表於2019-04-24

C/C++ 面試知識總結

這是一篇五萬字的C/C++面試知識點總結,包括答案:這是上篇,下篇今天也推送了,需要的同學記得去看看。本文花費了博主大量的時間進行收集、排版:如果你覺得文章對你有幫助幫忙點贊給博主一點鼓勵~~

目錄

  • C/C++

  • STL

  • 資料結構

  • 演算法

  • Problems

  • 作業系統

  • 計算機網路

  • 網路程式設計

  • 資料庫

  • 設計模式

  • 連結裝載庫

  • 海量資料處理

  • 音視訊

  • 其他

  • 書籍

  • 複習刷題網站

  • 招聘時間崗位

  • 面試題目經驗

C/C++

const

作用

  1. 修飾變數,說明該變數不可以被改變;

  2. 修飾指標,分為指向常量的指標和指標常量;

  3. 常量引用,經常用於形參型別,即避免了拷貝,又避免了函式對值的修改;

  4. 修飾成員函式,說明該成員函式內不能修改成員變數。

使用

// 類
class A
{
private:
    const int a;                // 常物件成員,只能在初始化列表賦值

public:
    // 建構函式
    A() { };
    A(int x) : a(x) { };        // 初始化列表

    // const可用於對過載函式的區分
    int getValue();             // 普通成員函式
    int getValue() const;       // 常成員函式,不得修改類中的任何資料成員的值
};

void function()
{
    // 物件
    A b;                        // 普通物件,可以呼叫全部成員函式
    const A a;                  // 常物件,只能呼叫常成員函式、更新常成員變數
    const A *p = &a;            // 常指標
    const A &q = a;             // 常引用

    // 指標
    char greeting[] = "Hello";
    char* p1 = greeting;                // 指標變數,指向字元陣列變數
    const char* p2 = greeting;          // 指標變數,指向字元陣列常量
    char* const p3 = greeting;          // 常指標,指向字元陣列變數
    const char* const p4 = greeting;    // 常指標,指向字元陣列常量
}

// 函式
void function1(const int Var);           // 傳遞過來的引數在函式內不可變
void function2(const char* Var);         // 引數指標所指內容為常量
void function3(char* const Var);         // 引數指標為常指標
void function4(const int& Var);          // 引用引數在函式內為常量

// 函式返回值
const int function5();      // 返回一個常數
const int* function6();     // 返回一個指向常量的指標變數,使用:const int *p = function6();
int* const function7();     // 返回一個指向變數的常指標,使用:int* const p = function7();
複製程式碼

static

作用

  1. 修飾普通變數,修改變數的儲存區域和生命週期,使變數儲存在靜態區,在 main 函式執行前就分配了空間,如果有初始值就用初始值初始化它,如果沒有初始值系統用預設值初始化它。

  2. 修飾普通函式,表明函式的作用範圍,僅在定義該函式的檔案內才能使用。在多人開發專案時,為了防止與他人命令函式重名,可以將函式定位為 static。

  3. 修飾成員變數,修飾成員變數使所有的物件只儲存一個該變數,而且不需要生成物件就可以訪問該成員。

  4. 修飾成員函式,修飾成員函式使得不需要生成物件就可以訪問該函式,但是在 static 函式內不能訪問非靜態成員。

this 指標

  1. this 指標是一個隱含於每一個非靜態成員函式中的特殊指標。它指向正在被該成員函式操作的那個物件。

  2. 當對一個物件呼叫成員函式時,編譯程式先將物件的地址賦給 this 指標,然後呼叫成員函式,每次成員函式存取資料成員時,由隱含使用 this 指標。

  3. 當一個成員函式被呼叫時,自動向它傳遞一個隱含的引數,該引數是一個指向這個成員函式所在的物件的指標。

  4. this 指標被隱含地宣告為: ClassName *const this,這意味著不能給 this 指標賦值;在 ClassName 類的 const 成員函式中,this 指標的型別為:const ClassName* const,這說明不能對 this 指標所指向的這種物件是不可修改的(即不能對這種物件的資料成員進行賦值操作);

  5. this 並不是一個常規變數,而是個右值,所以不能取得 this 的地址(不能 &this)。

  6. 在以下場景中,經常需要顯式引用 this 指標:

  7. 為實現物件的鏈式引用;

  8. 為避免對同一物件進行賦值操作;

  9. 在實現一些資料結構時,如 list

inline 行內函數

特徵

  • 相當於把行內函數裡面的內容寫在呼叫行內函數處;

  • 相當於不用執行進入函式的步驟,直接執行函式體;

  • 相當於巨集,卻比巨集多了型別檢查,真正具有函式特性;

  • 不能包含迴圈、遞迴、switch 等複雜操作;

  • 在類宣告中定義的函式,除了虛擬函式的其他函式都會自動隱式地當成行內函數。

使用

// 宣告1(加 inline,建議使用)
inline int functionName(int first, int secend,...);

// 宣告2(不加 inline)
int functionName(int first, int secend,...);

// 定義
inline int functionName(int first, int secend,...) {/****/};

// 類內定義,隱式內聯
class A {
    int doA() { return 0; }         // 隱式內聯
}

// 類外定義,需要顯式內聯
class A {
    int doA();
}
inline int A::doA() { return 0; }   // 需要顯式內聯
複製程式碼

編譯器對 inline 函式的處理步驟

  1. 將 inline 函式體複製到 inline 函式呼叫點處;

  2. 為所用 inline 函式中的區域性變數分配記憶體空間;

  3. 將 inline 函式的的輸入引數和返回值對映到呼叫方法的區域性變數空間中;

  4. 如果 inline 函式有多個返回點,將其轉變為 inline 函式程式碼塊末尾的分支(使用 GOTO)。

優缺點

優點

  1. 行內函數同巨集函式一樣將在被呼叫處進行程式碼展開,省去了引數壓棧、棧幀開闢與回收,結果返回等,從而提高程式執行速度。

  2. 行內函數相比巨集函式來說,在程式碼展開時,會做安全檢查或自動型別轉換(同普通函式),而巨集定義則不會。

  3. 在類中宣告同時定義的成員函式,自動轉化為行內函數,因此行內函數可以訪問類的成員變數,巨集定義則不能。

  4. 行內函數在執行時可除錯,而巨集定義不可以。

缺點

  1. 程式碼膨脹。內聯是以程式碼膨脹(複製)為代價,消除函式呼叫帶來的開銷。如果執行函式體內程式碼的時間,相比於函式呼叫的開銷較大,那麼效率的收穫會很少。另一方面,每一處行內函數的呼叫都要複製程式碼,將使程式的總程式碼量增大,消耗更多的記憶體空間。

  2. inline 函式無法隨著函式庫升級而升級。inline函式的改變需要重新編譯,不像 non-inline 可以直接連結。

  3. 是否內聯,程式設計師不可控。行內函數只是對編譯器的建議,是否對函式內聯,決定權在於編譯器。

虛擬函式(virtual)可以是行內函數(inline)嗎?

Are "inline virtual" member functions ever actually "inlined"?

答案:www.cs.technion.ac.il/users/yechi…

  • 虛擬函式可以是行內函數,內聯是可以修飾虛擬函式的,但是當虛擬函式表現多型性的時候不能內聯。

  • 內聯是在編譯器建議編譯器內聯,而虛擬函式的多型性在執行期,編譯器無法知道執行期呼叫哪個程式碼,因此虛擬函式表現為多型性時(執行期)不可以內聯。

  • inline virtual 唯一可以內聯的時候是:編譯器知道所呼叫的物件是哪個類(如 Base::who()),這隻有在編譯器具有實際物件而不是物件的指標或引用時才會發生。

虛擬函式內聯使用

#include <iostream>  
using namespace std;
class Base
{
public:
    inline virtual void who()
    {
        cout << "I am Base\n";
    }
    virtual ~Base() {}
};
class Derived : public Base
{
public:
    inline void who()  // 不寫inline時隱式內聯
    {
        cout << "I am Derived\n";
    }
};

int main()
{
    // 此處的虛擬函式 who(),是通過類(Base)的具體物件(b)來呼叫的,編譯期間就能確定了,所以它可以是內聯的,但最終是否內聯取決於編譯器。 
    Base b;
    b.who();

    // 此處的虛擬函式是通過指標呼叫的,呈現多型性,需要在執行時期間才能確定,所以不能為內聯。  
    Base *ptr = new Derived();
    ptr->who();

    // 因為Base有虛解構函式(virtual ~Base() {}),所以 delete 時,會先呼叫派生類(Derived)解構函式,再呼叫基類(Base)解構函式,防止記憶體洩漏。
    delete ptr;
    ptr = nullptr;

    system("pause");
    return 0;
} 
複製程式碼

assert()

斷言,是巨集,而非函式。assert 巨集的原型定義在 <assert.h>(C)、<cassert>(C++)中,其作用是如果它的條件返回錯誤,則終止程式執行。可以通過定義 NDEBUG 來關閉 assert,但是需要在原始碼的開頭,include <assert.h> 之前。

使用

#define NDEBUG          // 加上這行,則 assert 不可用
#include <assert.h>

assert( p != NULL );    // assert 不可用
複製程式碼

sizeof()

  • sizeof 對陣列,得到整個陣列所佔空間大小。

  • sizeof 對指標,得到指標本身所佔空間大小。

#pragma pack(n)

設定結構體、聯合以及類成員變數以 n 位元組方式對齊

使用

#pragma pack(push)  // 儲存對齊狀態
#pragma pack(4)     // 設定為 4 位元組對齊

struct test
{
    char m1;
    double m4;
    int m3;
};

#pragma pack(pop)   // 恢復對齊狀態
複製程式碼

位域

Bit mode: 2;    // mode 佔 2 位
複製程式碼

類可以將其(非靜態)資料成員定義為位域(bit-field),在一個位域中含有一定數量的二進位制位。當一個程式需要向其他程式或硬體裝置傳遞二進位制資料時,通常會用到位域。

  • 位域在記憶體中的佈局是與機器有關的

  • 位域的型別必須是整型或列舉型別,帶符號型別中的位域的行為將因具體實現而定

  • 取地址運算子(&)不能作用於位域,任何指標都無法指向類的位域

volatile

volatile int i = 10; 
複製程式碼
  • volatile 關鍵字是一種型別修飾符,用它宣告的型別變數表示可以被某些編譯器未知的因素(作業系統、硬體、其它執行緒等)更改。所以使用 volatile 告訴編譯器不應對這樣的物件進行優化。

  • volatile 關鍵字宣告的變數,每次訪問時都必須從記憶體中取出值(沒有被 volatile 修飾的變數,可能由於編譯器的優化,從 CPU 暫存器中取值)

  • const 可以是 volatile (如只讀的狀態暫存器)

  • 指標可以是 volatile

extern "C"

  • 被 extern 限定的函式或變數是 extern 型別的

  • 被 extern "C" 修飾的變數和函式是按照 C 語言方式編譯和連線的

extern "C" 的作用是讓 C++ 編譯器將 extern "C" 宣告的程式碼當作 C 語言程式碼處理,可以避免 C++ 因符號修飾導致程式碼不能和C語言庫中的符號進行連結的問題。

"C" 使用

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif
複製程式碼

struct 和 typedef struct

C 中

// c
typedef struct Student {
    int age; 
} S;
複製程式碼

等價於

// c
struct Student { 
    int age; 
};

typedef struct Student S;
複製程式碼

此時 S 等價於 struct Student,但兩個識別符號名稱空間不相同。

另外還可以定義與 struct Student 不衝突的 void Student() {}

C++ 中

由於編譯器定位符號的規則(搜尋規則)改變,導致不同於C語言。

一、如果在類識別符號空間定義了 struct Student {...};,使用 Student me; 時,編譯器將搜尋全域性識別符號表,Student 未找到,則在類識別符號內搜尋。

即表現為可以使用 Student 也可以使用 struct Student,如下:

// cpp
struct Student { 
    int age; 
};

void f( Student me );       // 正確,"struct" 關鍵字可省略
複製程式碼

二、若定義了與 Student 同名函式之後,則 Student 只代表函式,不代表結構體,如下:

typedef struct Student { 
    int age; 
} S;

void Student() {}           // 正確,定義後 "Student" 只代表此函式

//void S() {}               // 錯誤,符號 "S" 已經被定義為一個 "struct Student" 的別名

int main() {
    Student(); 
    struct Student me;      // 或者 "S me";
    return 0;
}
複製程式碼

C++ 中 struct 和 class

總的來說,struct 更適合看成是一個資料結構的實現體,class 更適合看成是一個物件的實現體。

區別

  • 最本質的一個區別就是預設的訪問控制
  1. 預設的繼承訪問許可權。struct 是 public 的,class 是 private 的。

  2. struct 作為資料結構的實現體,它預設的資料訪問控制是 public 的,而 class 作為物件的實現體,它預設的成員變數訪問控制是 private 的。

union 聯合

聯合(union)是一種節省空間的特殊的類,一個 union 可以有多個資料成員,但是在任意時刻只有一個資料成員可以有值。當某個成員被賦值後其他成員變為未定義狀態。聯合有如下特點:

  • 預設訪問控制符為 public

  • 可以含有建構函式、解構函式

  • 不能含有引用型別的成員

  • 不能繼承自其他類,不能作為基類

  • 不能含有虛擬函式

  • 匿名 union 在定義所在作用域可直接訪問 union 成員

  • 匿名 union 不能包含 protected 成員或 private 成員

  • 全域性匿名聯合必須是靜態(static)的

    使用

#include<iostream>

union UnionTest {
    UnionTest() : i(10) {};
    int i;
    double d;
};

static union {
    int i;
    double d;
};

int main() {
    UnionTest u;

    union {
        int i;
        double d;
    };

    std::cout << u.i << std::endl;  // 輸出 UnionTest 聯合的 10

    ::i = 20;
    std::cout << ::i << std::endl;  // 輸出全域性靜態匿名聯合的 20

    i = 30;
    std::cout << i << std::endl;    // 輸出區域性匿名聯合的 30

    return 0;
}
複製程式碼

C 實現 C++ 類

C 語言實現封裝、繼承和多型:

dongxicheng.org/cpp/ooc/

explicit(顯式)建構函式

explicit 修飾的建構函式可用來防止隱式轉換

explicit 使用

class Test1
{
public:
    Test1(int n)            // 普通建構函式
    {
        num=n;
    }
private:
    int num;
};

class Test2
{
public:
    explicit Test2(int n)   // explicit(顯式)建構函式
    {
        num=n;
    }
private:
    int num;
};

int main()
{
    Test1 t1=12;            // 隱式呼叫其建構函式,成功
    Test2 t2=12;            // 編譯錯誤,不能隱式呼叫其建構函式
    Test2 t2(12);           // 顯式呼叫成功
    return 0;
}
複製程式碼

friend 友元類和友元函式

  • 能訪問私有成員

  • 破壞封裝性

  • 友元關係不可傳遞

  • 友元關係的單向性

  • 友元宣告的形式及數量不受限制

using

using 宣告

一條 using 宣告 語句一次只引入名稱空間的一個成員。它使得我們可以清楚知道程式中所引用的到底是哪個名字。如:

using namespace_name::name;
複製程式碼

建構函式的 using 宣告【C++11】

在 C++11 中,派生類能夠重用其直接基類定義的建構函式。

class Derived : Base {
public:
    using Base::Base;
    /* ... */
};
複製程式碼

如上 using 宣告,對於基類的每個建構函式,編譯器都生成一個與之對應(形參列表完全相同)的派生類建構函式。生成如下型別建構函式:

derived(parms) : base(args) { }
複製程式碼

using 指示

using 指示 使得某個特定名稱空間中所有名字都可見,這樣我們就無需再為它們新增任何字首限定符了。如:

using namespace_name name;
複製程式碼

儘量少使用 using 指示 汙染名稱空間

一般說來,使用 using 命令比使用 using 編譯命令更安全,這是由於它只匯入了制定的名稱。如果該名稱與區域性名稱發生衝突,編譯器將發出指示。using編譯命令匯入所有的名稱,包括可能並不需要的名稱。如果與區域性名稱發生衝突,則區域性名稱將覆蓋名稱空間版本,而編譯器並不會發出警告。另外,名稱空間的開放性意味著名稱空間的名稱可能分散在多個地方,這使得難以準確知道新增了哪些名稱。

using 使用

儘量少使用 using 指示

using namespace std;
複製程式碼

應該多使用 using 宣告

int x;
std::cin >> x ;
std::cout << x << std::endl;
複製程式碼

或者

using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;
複製程式碼

:: 範圍解析運算子

分類

  1. 全域性作用域符(::name):用於型別名稱(類、類成員、成員函式、變數等)前,表示作用域為全域性名稱空間

  2. 類作用域符(class::name):用於表示指定型別的作用域範圍是具體某個類的

  3. 名稱空間作用域符(namespace::name):用於表示指定型別的作用域範圍是具體某個名稱空間的

:: 使用

int count = 0;        // 全域性(::)的 count

class A {
public:
    static int count; // 類 A 的 count(A::count)
};

int main() {
    ::count = 1;      // 設定全域性的 count 的值為 1

    A::count = 2;     // 設定類 A 的 count 為 2

    int count = 0;    // 區域性的 count
    count = 3;        // 設定區域性的 count 的值為 3

    return 0;
}
複製程式碼

enum 列舉型別

限定作用域的列舉型別

enum class open_modes { input, output, append };
複製程式碼

不限定作用域的列舉型別

enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };
複製程式碼

decltype

decltype 關鍵字用於檢查實體的宣告型別或表示式的型別及值分類。語法:

decltype ( expression )
複製程式碼

使用

// 尾置返回允許我們在引數列表之後宣告返回型別
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
    // 處理序列
    return *beg;    // 返回序列中一個元素的引用
}
// 為了使用模板引數成員,必須用 typename
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
    // 處理序列
    return *beg;    // 返回序列中一個元素的拷貝
}
複製程式碼

引用

左值引用

常規引用,一般表示物件的身份。

右值引用

右值引用就是必須繫結到右值(一個臨時物件、將要銷燬的物件)的引用,一般表示物件的值。

右值引用可實現轉移語義(Move Sementics)和精確傳遞(Perfect Forwarding),它的主要目的有兩個方面:

  • 消除兩個物件互動時不必要的物件拷貝,節省運算儲存資源,提高效率。

  • 能夠更簡潔明確地定義泛型函式。

引用摺疊

  • X& &、X& &&、X&& & 可摺疊成 X&

  • X&& && 可摺疊成 X&&

巨集

  • 巨集定義可以實現類似於函式的功能,但是它終歸不是函式,而巨集定義中括弧中的“引數”也不是真的引數,在巨集展開的時候對 “引數” 進行的是一對一的替換。

成員初始化列表

好處

  • 更高效:少了一次呼叫預設建構函式的過程。

  • 有些場合必須要用初始化列表:

  1. 常量成員,因為常量只能初始化不能賦值,所以必須放在初始化列表裡面

  2. 引用型別,引用必須在定義的時候初始化,並且不能重新賦值,所以也要寫在初始化列表裡面

  3. 沒有預設建構函式的類型別,因為使用初始化列表可以不必呼叫預設建構函式來初始化,而是直接呼叫拷貝建構函式初始化。

initializer_list 列表初始化【C++11】

用花括號初始化器列表列表初始化一個物件,其中對應建構函式接受一個 std::initializer_list 引數.

initializer_list 使用

#include <iostream>
#include <vector>
#include <initializer_list>

template <class T>
struct S {
    std::vector<T> v;
    S(std::initializer_list<T> l) : v(l) {
         std::cout << "constructed with a " << l.size() << "-element list\n";
    }
    void append(std::initializer_list<T> l) {
        v.insert(v.end(), l.begin(), l.end());
    }
    std::pair<const T*, std::size_t> c_arr() const {
        return {&v[0], v.size()};  // 在 return 語句中複製列表初始化
                                   // 這不使用 std::initializer_list
    }
};

template <typename T>
void templated_fn(T) {}

int main()
{
    S<int> s = {1, 2, 3, 4, 5}; // 複製初始化
    s.append({6, 7, 8});      // 函式呼叫中的列表初始化

    std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";

    for (auto n : s.v)
        std::cout << n << ' ';
    std::cout << '\n';

    std::cout << "Range-for over brace-init-list: \n";

    for (int x : {-1, -2, -3}) // auto 的規則令此帶範圍 for 工作
        std::cout << x << ' ';
    std::cout << '\n';

    auto al = {10, 11, 12};   // auto 的特殊規則

    std::cout << "The list bound to auto has size() = " << al.size() << '\n';

//    templated_fn({1, 2, 3}); // 編譯錯誤!“ {1, 2, 3} ”不是表示式,
                             // 它無型別,故 T 無法推導
    templated_fn<std::initializer_list<int>>({1, 2, 3}); // OK
    templated_fn<std::vector<int>>({1, 2, 3});           // 也 OK
}
複製程式碼

物件導向

物件導向程式設計(Object-oriented programming,OOP)是種具有物件概念的程式程式設計典範,同時也是一種程式開發的抽象方針。

五萬字長文 C C++ 面試知識總結(上)
物件導向特徵

物件導向三大特徵 —— 封裝、繼承、多型

封裝

  • 把客觀事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的進行資訊隱藏。

  • 關鍵字:public, protected, friendly, private。不寫預設為 friendly。

關鍵字 當前類 包內 子孫類 包外
public
protected ×
friendly × ×
private × × ×

繼承

  • 基類(父類)——> 派生類(子類)

多型

  • 多型,即多種狀態,在面嚮物件語言中,介面的多種不同的實現方式即為多型。

  • C++ 多型有兩種:靜態多型(早繫結)、動態多型(晚繫結)。靜態多型是通過函式過載實現的;動態多型是通過虛擬函式實現的。

  • 多型是以封裝和繼承為基礎的。

靜態多型(早繫結)

函式過載

class A
{
public:
    void do(int a);
    void do(int a, int b);
};
複製程式碼

動態多型(晚繫結)

  • 虛擬函式:用 virtual 修飾成員函式,使其成為虛擬函式

注意:

  • 普通函式(非類成員函式)不能是虛擬函式

  • 靜態函式(static)不能是虛擬函式

  • 建構函式不能是虛擬函式(因為在呼叫建構函式時,虛表指標並沒有在物件的記憶體空間中,必須要建構函式呼叫完成後才會形成虛表指標)

  • 行內函數不能是表現多型性時的虛擬函式,解釋見:虛擬函式(virtual)可以是行內函數(inline)嗎?:t.cn/E4WVXSP

動態多型使用

class Shape                     // 形狀類
{
public:
    virtual double calcArea()
    {
        ...
    }
    virtual ~Shape();
};
class Circle : public Shape     // 圓形類
{
public:
    virtual double calcArea();
    ...
};
class Rect : public Shape       // 矩形類
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    Shape * shape2 = new Rect(5.0, 6.0);
    shape1->calcArea();         // 呼叫圓形類裡面的方法
    shape2->calcArea();         // 呼叫矩形類裡面的方法
    delete shape1;
    shape1 = nullptr;
    delete shape2;
    shape2 = nullptr;
    return 0;
}
複製程式碼

虛解構函式

虛解構函式是為了解決基類的指標指向派生類物件,並用基類的指標刪除派生類物件。

虛解構函式使用

class Shape
{
public:
    Shape();                    // 建構函式不能是虛擬函式
    virtual double calcArea();
    virtual ~Shape();           // 虛解構函式
};
class Circle : public Shape     // 圓形類
{
public:
    virtual double calcArea();
    ...
};
int main()
{
    Shape * shape1 = new Circle(4.0);
    shape1->calcArea();    
    delete shape1;  // 因為Shape有虛解構函式,所以delete釋放記憶體時,先呼叫子類解構函式,再呼叫基類解構函式,防止記憶體洩漏。
    shape1 = NULL;
    return 0;
}
複製程式碼

純虛擬函式

純虛擬函式是一種特殊的虛擬函式,在基類中不能對虛擬函式給出有意義的實現,而把它宣告為純虛擬函式,它的實現留給該基類的派生類去做。

virtual int A() = 0;
複製程式碼

虛擬函式、純虛擬函式

CSDN . C++ 中的虛擬函式、純虛擬函式區別和聯絡:t.cn/E4WVQBI

  • 類裡如果宣告瞭虛擬函式,這個函式是實現的,哪怕是空實現,它的作用就是為了能讓這個函式在它的子類裡面可以被覆蓋,這樣的話,這樣編譯器就可以使用後期繫結來達到多型了。純虛擬函式只是一個介面,是個函式的宣告而已,它要留到子類裡去實現。

  • 虛擬函式在子類裡面也可以不過載的;但純虛擬函式必須在子類去實現。

  • 虛擬函式的類用於 “實作繼承”,繼承介面的同時也繼承了父類的實現。當然大家也可以完成自己的實現。純虛擬函式關注的是介面的統一性,實現由子類完成。

  • 帶純虛擬函式的類叫抽象類,這種類不能直接生成物件,而只有被繼承,並重寫其虛擬函式後,才能使用。抽象類和大家口頭常說的虛基類還是有區別的,在 C# 中用 abstract 定義抽象類,而在 C++ 中有抽象類的概念,但是沒有這個關鍵字。抽象類被繼承後,子類可以繼續是抽象類,也可以是普通類,而虛基類,是含有純虛擬函式的類,它如果被繼承,那麼子類就必須實現虛基類裡面的所有純虛擬函式,其子類不能是抽象類。

虛擬函式指標、虛擬函式表

  • 虛擬函式指標:在含有虛擬函式類的物件中,指向虛擬函式表,在執行時確定。

  • 虛擬函式表:在程式只讀資料段(.rodata section,見:目標檔案儲存結構:t.cn/E4WVBeF),存放…

虛繼承

虛繼承用於解決多繼承條件下的菱形繼承問題(浪費儲存空間、存在二義性)。

底層實現原理與編譯器相關,一般通過虛基類指標虛基類表實現,每個虛繼承的子類都有一個虛基類指標(佔用一個指標的儲存空間,4位元組)和虛基類表(不佔用類物件的儲存空間)(需要強調的是,虛基類依舊會在子類裡面存在拷貝,只是僅僅最多存在一份而已,並不是不在子類裡面了);當虛繼承的子類被當做父類繼承時,虛基類指標也會被繼承。

實際上,vbptr 指的是虛基類表指標(virtual base table pointer),該指標指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持著公共基類(虛基類)的兩份同樣的拷貝,節省了儲存空間。

虛繼承、虛擬函式

  • 相同之處:都利用了虛指標(均佔用類的儲存空間)和虛表(均不佔用類的儲存空間)

  • 不同之處:

  • 虛擬函式不佔用儲存空間

  • 虛擬函式表儲存的是虛擬函式地址

  • 虛基類依舊存在繼承類中,只佔用儲存空間

  • 虛基類表儲存的是虛基類相對直接繼承類的偏移

  • 虛繼承

  • 虛擬函式

模板類、成員模板、虛擬函式

  • 模板類中可以使用虛擬函式

  • 一個類(無論是普通類還是類别範本)的成員模板(本身是模板的成員函式)不能是虛擬函式

抽象類、介面類、聚合類

  • 抽象類:含有純虛擬函式的類

  • 介面類:僅含有純虛擬函式的抽象類

  • 聚合類:使用者可以直接訪問其成員,並且具有特殊的初始化語法形式。滿足如下特點:

  • 所有成員都是 public

  • 沒有有定於任何建構函式

  • 沒有類內初始化

  • 沒有基類,也沒有 virtual 函式

記憶體分配和管理

malloc、calloc、realloc、alloca

  1. malloc:申請指定位元組數的記憶體。申請到的記憶體中的初始值不確定。

  2. calloc:為指定長度的物件,分配能容納其指定個數的記憶體。申請到的記憶體的每一位(bit)都初始化為 0。

  3. realloc:更改以前分配的記憶體長度(增加或減少)。當增加長度時,可能需將以前分配區的內容移到另一個足夠大的區域,而新增區域內的初始值則不確定。

  4. alloca:在棧上申請記憶體。程式在出棧的時候,會自動釋放記憶體。但是需要注意的是,alloca 不具可移植性, 而且在沒有傳統堆疊的機器上很難實現。alloca 不宜使用在必須廣泛移植的程式中。C99 中支援變長陣列 (VLA),可以用來替代 alloca。

malloc、free

用於分配、釋放記憶體

malloc、free 使用

申請記憶體,確認是否申請成功

char *str = (char*) malloc(100);
assert(str != nullptr);
複製程式碼

釋放記憶體後指標置空

free(p); 
p = nullptr;
複製程式碼

new、delete

  1. new / new[]:完成兩件事,先底層呼叫 malloc 分了配記憶體,然後呼叫建構函式(建立物件)。

  2. delete/delete[]:也完成兩件事,先呼叫解構函式(清理資源),然後底層呼叫 free 釋放空間。

  3. new 在申請記憶體時會自動計算所需位元組數,而 malloc 則需我們自己輸入申請記憶體空間的位元組數。

new、delete 使用

申請記憶體,確認是否申請成功

int main()
{
    T* t = new T();     // 先記憶體分配 ,再建構函式
    delete t;           // 先解構函式,再記憶體釋放
    return 0;
}
複製程式碼

定位 new

定位 new(placement new)允許我們向 new 傳遞額外的引數。

new (palce_address) type
new (palce_address) type (initializers)
new (palce_address) type [size]
new (palce_address) type [size] { braced initializer list }
複製程式碼
  • palce_address 是個指標

  • initializers 提供一個(可能為空的)以逗號分隔的初始值列表

delete this 合法嗎?

Is it legal (and moral) for a member function to say delete this? 答案:t.cn/E4Wfcfl

合法,但:

  1. 必須保證 this 物件是通過 new(不是 new[]、不是 placement new、不是棧上、不是全域性、不是其他物件成員)分配的

  2. 必須保證呼叫 delete this 的成員函式是最後一個呼叫 this 的成員函式

  3. 必須保證成員函式的 delete this 後面沒有呼叫 this 了

  4. 必須保證 delete this 後沒有人使用了

如何定義一個只能在堆上(棧上)生成物件的類?

如何定義一個只能在堆上(棧上)生成物件的類?

答案:t.cn/E4WfDhP

只能在堆上

方法:將解構函式設定為私有

原因:C++ 是靜態繫結語言,編譯器管理棧上物件的生命週期,編譯器在為類物件分配棧空間時,會先檢查類的解構函式的訪問性。若解構函式不可訪問,則不能在棧上建立物件。

只能在棧上

方法:將 new 和 delete 過載為私有

原因:在堆上生成物件,使用 new 關鍵詞操作,其過程分為兩階段:第一階段,使用 new 在堆上尋找可用記憶體,分配給物件;第二階段,呼叫建構函式生成物件。將 new 操作設定為私有,那麼第一階段就無法完成,就不能夠在堆上生成物件。

智慧指標

C++ 標準庫(STL)中

標頭檔案:#include <memory>

C++ 98

std::auto_ptr<std::string> ps (new std::string(str));
複製程式碼

受限於文章字數限制,後續部分請看【中篇】。


掃描下方二維碼,及時獲取更多網際網路求職面經javapython爬蟲大資料等技術,和海量資料分享:公眾號後臺回覆“csdn”即可免費領取【csdn】和【百度文庫】下載服務;公眾號後臺回覆“資料”:即可領取5T精品學習資料java面試考點java面經總結,以及幾十個java、大資料專案資料很全,你想找的幾乎都有

掃碼關注,及時獲取更多精彩內容。(博主今日頭條大資料工程師)

推薦閱讀

【微信事業群】二面面經

相關文章