天天看點

C語言與C++面試知識總結

這是一篇 C 語言與 C++面試知識點總結的文章,如果你覺得文章對你有幫助,文末右下角點個再看轉發給更多的人。

const

作用

  1. 修飾變量,說明該變量不可以被改變;
  2. 修飾指針,分為指向常量的指針(pointer to const)和自身是常量的指針(常量指針,const pointer);
  3. 修飾引用,指向常量的引用(reference to const),用于形參類型,即避免了拷貝,又避免了函數對值的修改;
  4. 修飾成員函數,說明該成員函數内不能修改成員變量。

const 的指針與引用

  1. 指針
  • 指向常量的指針(pointer to const)
  • 自身是常量的指針(常量指針,const pointer)
  1. 引用
  • 指向常量的引用(reference to const)
  • 沒有 const reference,因為引用本身就是 const pointer

(為了友善記憶可以想成)被 const 修飾(在 const 後面)的值不可改變,如下文使用例子中的 p2、p3。

使用

// 類
class A
{
private:
    const int a;                // 常對象成員,隻能在初始化清單指派

public:
    // 構造函數
    A() : a(0) { };
    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;          // 指針變量,指向字元數組常量(const 後面是 char,說明指向的字元(char)不可改變)
    char* const p3 = greeting;          // 自身是常量的指針,指向字元數組變量(const 後面是 p3,說明 p3 指針自身不可改變)
    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)。

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

  • 為實作對象的鍊式引用;
  • 為避免對同一對象進行指派操作;
  • 在實作一些資料結構時,如 list。

inline 内聯函數

特征

  • 相當于把内聯函數裡面的内容寫在調用内聯函數處;
  • 相當于不用執行進入函數的步驟,直接執行函數體;
  • 相當于宏,卻比宏多了類型檢查,真正具有函數特性;
  • 編譯器一般不内聯包含循環、遞歸、switch 等複雜操作的内聯函數;
  • 在類聲明中定義的函數,除了虛函數的其他函數都會自動隐式地當成内聯函數。

inline 使用

// 聲明1(加 inline,建議使用)
inline int functionName(int first, int second,...);

// 聲明2(不加 inline)
int functionName(int first, int second,...);

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

// 類内定義,隐式内聯
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)。

優缺點

優點

  • 内聯函數同宏函數一樣将在被調用處進行代碼展開,省去了參數壓棧、棧幀開辟與回收,結果傳回等,進而提高程式運作速度。
  • 内聯函數相比宏函數來說,在代碼展開時,會做安全檢查或自動類型轉換(同普通函數),而宏定義則不會。
  • 在類中聲明同時定義的成員函數,自動轉化為内聯函數,是以内聯函數可以通路類的成員變量,宏定義則不能。
  • 内聯函數在運作時可調試,而宏定義不可以。

虛函數(virtual)可以是内聯函數(inline)嗎?

Are "inline virtual" member functions ever actually "inlined"?
  • 虛函數可以是内聯函數,内聯是可以修飾虛函數的,但是當虛函數表現多态性的時候不能内聯。
  • 内聯是在編譯器建議編譯器内聯,而虛函數的多态性在運作期,編譯器無法知道運作期調用哪個代碼,是以虛函數表現為多态性時(運作期)不可以内聯。
  • 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;
}
           

volatile

volatile int i = 10;

  • volatile 關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素(作業系統、硬體、其它線程等)更改。是以使用 volatile 告訴編譯器不應對這樣的對象進行優化。
  • volatile 關鍵字聲明的變量,每次通路時都必須從記憶體中取出值(沒有被 volatile 修飾的變量,可能由于編譯器的優化,從 CPU 寄存器中取值)
  • const 可以是 volatile (如隻讀的狀态寄存器)
  • 指針可以是 volatile

assert()

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

assert() 使用

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

assert( p != NULL );    // assert 不可用
           

sizeof()

  • sizeof 對數組,得到整個數組所占空間大小。
  • sizeof 對指針,得到指針本身所占空間大小。

#pragma pack(n)

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

#pragma pack(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),在一個位域中含有一定數量的二進制位。當一個程式需要向其他程式或硬體裝置傳遞二進制資料時,通常會用到位域。

  • 位域在記憶體中的布局是與機器有關的
  • 位域的類型必須是整型或枚舉類型,帶符号類型中的位域的行為将因具體實作而定
  • 取位址運算符(&)不能作用于位域,任何指針都無法指向類的位域

extern "C"

  • 被 extern 限定的函數或變量是 extern 類型的
  • 被 extern "C" 修飾的變量和函數是按照 C 語言方式編譯和連結的

extern "C" 的作用是讓 C++ 編譯器将 extern "C" 聲明的代碼當作 C 語言代碼處理,可以避免 C++ 因符号修飾導緻代碼不能和C語言庫中的符号進行連結的問題。

extern "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語言。

1.如果在類辨別符空間定義了 struct Student {...};,使用 Student me; 時,編譯器将搜尋全局辨別符表,Student 未找到,則在類辨別符内搜尋。

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

// cpp
struct Student {
    int age;
};

void f( Student me );       // 正确,"struct" 關鍵字可省略
           

2.若定義了與 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 更适合看成是一個對象的實作體。

差別:

最本質的一個差別就是預設的通路控制

  • 預設的繼承通路權限。struct 是 public 的,class 是 private 的。
  • struct 作為資料結構的實作體,它預設的資料通路控制是 public 的,而 class 作為對象的實作體,它預設的成員變量通路控制是 private 的。

union 聯合

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

  • 預設通路控制符為 public
  • 可以含有構造函數、析構函數
  • 不能含有引用類型的成員
  • 不能繼承自其他類,不能作為基類
  • 不能含有虛函數
  • 匿名 union 在定義所在作用域可直接通路 union 成員
  • 匿名 union 不能包含 protected 成員或 private 成員
  • 全局匿名聯合必須是靜态(static)的

union 使用

#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 實作 C++ 的面向對象特性(封裝、繼承、多态)

  • 封裝:使用函數指針把屬性與方法封裝到結構體中
  • 繼承:結構體嵌套
  • 多态:父類與子類方法的函數指針不同

explicit(顯式)關鍵字

  • explicit 修飾構造函數時,可以防止隐式轉換和複制初始化
  • explicit 修飾轉換函數時,可以防止隐式轉換,但 按語境轉換 除外

explicit 使用

struct A
{
	A(int) { }
	operator bool() const { return true; }
};

struct B
{
	explicit B(int) {}
	explicit operator bool() const { return true; }
};

void doA(A a) {}

void doB(B b) {}

int main()
{
	A a1(1);		// OK:直接初始化
	A a2 = 1;		// OK:複制初始化
	A a3{ 1 };		// OK:直接清單初始化
	A a4 = { 1 };		// OK:複制清單初始化
	A a5 = (A)1;		// OK:允許 static_cast 的顯式轉換
	doA(1);			// OK:允許從 int 到 A 的隐式轉換
	if (a1);		// OK:使用轉換函數 A::operator bool() 的從 A 到 bool 的隐式轉換
	bool a6(a1);		// OK:使用轉換函數 A::operator bool() 的從 A 到 bool 的隐式轉換
	bool a7 = a1;		// OK:使用轉換函數 A::operator bool() 的從 A 到 bool 的隐式轉換
	bool a8 = static_cast<bool>(a1);  // OK :static_cast 進行直接初始化

	B b1(1);		// OK:直接初始化
	B b2 = 1;		// 錯誤:被 explicit 修飾構造函數的對象不可以複制初始化
	B b3{ 1 };		// OK:直接清單初始化
	B b4 = { 1 };		// 錯誤:被 explicit 修飾構造函數的對象不可以複制清單初始化
	B b5 = (B)1;		// OK:允許 static_cast 的顯式轉換
	doB(1);			// 錯誤:被 explicit 修飾構造函數的對象不可以從 int 到 B 的隐式轉換
	if (b1);		// OK:被 explicit 修飾轉換函數 B::operator bool() 的對象可以從 B 到 bool 的按語境轉換
	bool b6(b1);		// OK:被 explicit 修飾轉換函數 B::operator bool() 的對象可以從 B 到 bool 的按語境轉換
	bool b7 = b1;		// 錯誤:被 explicit 修飾轉換函數 B::operator bool() 的對象不可以隐式轉換
	bool b8 = static_cast<bool>(b1);  // OK:static_cast 進行直接初始化

	return 0;
}
           

friend 友元類和友元函數

  • 能通路私有成員
  • 破壞封裝性
  • 友元關系不可傳遞
  • 友元關系的單向性
  • 友元聲明的形式及數量不受限制

using

using 聲明

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

using namespace_name::name;

構造函數的 using 聲明

在 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 = 11;         // 全局(::)的 count

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

void fun()
{
	int count = 31;     // 初始化局部的 count 為 31
	count = 32;         // 設定局部的 count 的值為 32
}

int main() {
	::count = 12;       // 測試 1:設定全局的 count 的值為 12

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

	fun();		        // 測試 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 )

decltype 使用

// 尾置傳回允許我們在參數清單之後聲明傳回類型
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. 有些場合必須要用初始化清單:
  • 常量成員,因為常量隻能初始化不能指派,是以必須放在初始化清單裡面
  • 引用類型,引用必須在定義的時候初始化,并且不能重新指派,是以也要寫在初始化清單裡面
  • 沒有預設構造函數的類類型,因為使用初始化清單可以不必調用預設構造函數來初始化

initializer_list 清單初始化

用花括号初始化器清單初始化一個對象,其中對應構造函數接受一個 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)是種具有對象概念的程式程式設計典範,同時也是一種程式開發的抽象方針。

面向對象特征

面向對象三大特征 —— 封裝、繼承、多态

封裝

把客觀事物封裝成抽象的類,并且類可以把自己的資料和方法隻讓可信的類或者對象操作,對不可信的進行資訊隐藏。關鍵字:public, protected, private。不寫預設為 private。

  • public 成員:可以被任意實體通路
  • protected 成員:隻允許被子類及本類的成員函數通路
  • private 成員:隻允許被本類的成員函數、友元類或友元函數通路

繼承

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

多态

  1. 多态,即多種狀态(形态)。簡單來說,我們可以将多态定義為消息以多種形式顯示的能力。
  2. 多态是以封裝和繼承為基礎的。
  3. C++ 多态分類及實作:
  • 重載多态(Ad-hoc Polymorphism,編譯期):函數重載、運算符重載
  • 子類型多态(Subtype Polymorphism,運作期):虛函數
  • 參數多态性(Parametric Polymorphism,編譯期):類模闆、函數模闆
  • 強制多态(Coercion Polymorphism,編譯期/運作期):基本類型轉換、自定義類型轉換

靜态多态(編譯期/早綁定)

函數重載

class A
{
public:
    void do(int a);
    void do(int a, int b);
};
           

動态多态(運作期期/晚綁定)

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

注意:

  • 普通函數(非類成員函數)不能是虛函數
  • 靜态函數(static)不能是虛函數
  • 構造函數不能是虛函數(因為在調用構造函數時,虛表指針并沒有在對象的記憶體空間中,必須要構造函數調用完成後才會形成虛表指針)
  • 内聯函數不能是表現多态性時的虛函數,解釋見:虛函數(virtual)可以是内聯函數(inline)嗎?

動态多态使用

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;

虛函數、純虛函數

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

虛函數指針、虛函數表

  • 虛函數指針:在含有虛函數類的對象中,指向虛函數表,在運作時确定。
  • 虛函數表:在程式隻讀資料段(.rodata section,見:目标檔案存儲結構),存放虛函數指針,如果派生類實作了基類的某個虛函數,則在虛表中覆寫原本基類的那個虛函數指針,在編譯時根據類的聲明建立。

虛繼承

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

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

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

虛繼承、虛函數

  1. 相同之處:都利用了虛指針(均占用類的存儲空間)和虛表(均不占用類的存儲空間)
  2. 不同之處:
  • 虛基類依舊存在繼承類中,隻占用存儲空間
  • 虛基類表存儲的是虛基類相對直接繼承類的偏移

虛函數

  • 虛函數不占用存儲空間
  • 虛函數表存儲的是虛函數位址

模闆類、成員模闆、虛函數

  • 模闆類中可以使用虛函數
  • 一個類(無論是普通類還是類模闆)的成員模闆(本身是模闆的成員函數)不能是虛函數

抽象類、接口類、聚合類

抽象類:含有純虛函數的類

接口類:僅含有純虛函數的抽象類

聚合類:使用者可以直接通路其成員,并且具有特殊的初始化文法形式。滿足如下特點:

  • 所有成員都是 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 (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
           
  • place_address 是個指針
  • initializers 提供一個(可能為空的)以逗号分隔的初始值清單

delete this 合法嗎?

合法,但:

  • 必須保證 this 對象是通過 new(不是 new[]、不是 placement new、不是棧上、不是全局、不是其他對象成員)配置設定的
  • 必須保證調用 delete this 的成員函數是最後一個調用 this 的成員函數
  • 必須保證成員函數的 delete this 後面沒有調用 this 了
  • 必須保證 delete this 後沒有人使用了

定義隻在堆(棧)生成對象類

隻能在堆上

方法:将析構函數設定為私有

原因:C++ 是靜态綁定語言,編譯器管理棧上對象的生命周期,編譯器在為類對象配置設定棧空間時,會先檢查類的析構函數的通路性。若析構函數不可通路,則不能在棧上建立對象。

能在棧上

方法:将 new 和 delete 重載為私有

原因:在堆上生成對象,使用 new 關鍵詞操作,其過程分為兩階段:第一階段,使用 new 在堆上尋找可用記憶體,配置設定給對象;第二階段,調用構造函數生成對象。将 new 操作設定為私有,那麼第一階段就無法完成,就不能夠在堆上生成對象。

智能指針

C++ 标準庫(STL)中

頭檔案:#include <memory>

C++ 98

std::auto_ptr<std::string> ps (new std::string(str));
           

C++ 11

對于該論述,歡迎讀者查閱之前發過的文章,你是《未來世界的幸存者》麼?

3.7 分割線

可以在一行中用三個以上的減号來建立一個分隔線,同時需要在分隔線的上面空一行。如下:

  1. shared_ptr
  2. unique_ptr
  3. weak_ptr
  4. auto_ptr(被 C++11 棄用)
  • Class shared_ptr 實作共享式擁有(shared ownership)概念。多個智能指針指向相同對象,該對象和其相關資源會在 “最後一個 reference 被銷毀” 時被釋放。為了在結構較複雜的情景中執行上述工作,标準庫提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等輔助類。
  • Class unique_ptr 實作獨占式擁有(exclusive ownership)或嚴格擁有(strict ownership)概念,保證同一時間内隻有一個智能指針可以指向該對象。你可以移交擁有權。它對于避免記憶體洩漏(resource leak)——如 new 後忘記 delete ——特别有用。

多個智能指針可以共享同一個對象,對象的最末一個擁有着有責任銷毀對象,并清理與該對象相關的所有資源。

  • 支援定制型删除器(custom deleter),可防範 Cross-DLL 問題(對象在動态連結庫(DLL)中被 new 建立,卻在另一個 DLL 内被 delete 銷毀)、自動解除互斥鎖

weak_ptr 允許你共享但不擁有某對象,一旦最末一個擁有該對象的智能指針失去了所有權,任何 weak_ptr 都會自動成空(empty)。是以,在 default 和 copy 構造函數之外,weak_ptr 隻提供 “接受一個 shared_ptr” 的構造函數。

  • 可打破環狀引用(cycles of references,兩個其實已經沒有被使用的對象彼此互指,使之看似還在 “被使用” 的狀态)的問題

unique_ptr 是 C++11 才開始提供的類型,是一種在異常時可以幫助避免資源洩漏的智能指針。采用獨占式擁有,意味着可以確定一個對象和其相應的資源同一時間隻被一個 pointer 擁有。一旦擁有着被銷毀或程式設計 empty,或開始擁有另一個對象,先前擁有的那個對象就會被銷毀,其任何相應資源亦會被釋放。

  • unique_ptr 用于取代 auto_ptr

auto_ptr

被 c++11 棄用,原因是缺乏語言特性如 “針對構造和指派” 的 std::move 語義,以及其他瑕疵。

auto_ptr 與 unique_ptr 比較

  • auto_ptr 可以指派拷貝,複制拷貝後所有權轉移;unqiue_ptr 無拷貝指派語義,但實作了move 語義;
  • auto_ptr 對象不能管理數組(析構調用 delete),unique_ptr 可以管理數組(析構調用 delete[] );

強制類型轉換運算符

static_cast

  • 用于非多态類型的轉換
  • 不執行運作時類型檢查(轉換安全性不如 dynamic_cast)
  • 通常用于轉換數值資料類型(如 float -> int)
  • 可以在整個類層次結構中移動指針,子類轉化為父類安全(向上轉換),父類轉化為子類不安全(因為子類可能有不在父類的字段或方法)

dynamic_cast

  • 用于多态類型的轉換
  • 執行行運作時類型檢查
  • 隻适用于指針或引用
  • 對不明确的指針的轉換将失敗(傳回 nullptr),但不引發異常
  • 可以在整個類層次結構中移動指針,包括向上轉換、向下轉換

const_cast

  • 用于删除 const、volatile 和 __unaligned 特性(如将 const int 類型轉換為 int 類型 )

reinterpret_cast

  • 用于位的簡單重新解釋
  • 濫用 reinterpret_cast 運算符可能很容易帶來風險。除非所需轉換本身是低級别的,否則應使用其他強制轉換運算符之一。
  • 允許将任何指針轉換為任何其他指針類型(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之類的轉換,但其本身并不安全)
  • 也允許将任何整數類型轉換為任何指針類型以及反向轉換。
  • reinterpret_cast 運算符不能丢掉 const、volatile 或 __unaligned 特性。
  • reinterpret_cast 的一個實際用途是在哈希函數中,即,通過讓兩個不同的值幾乎不以相同的索引結尾的方式将值映射到索引。

bad_cast

  • 由于強制轉換為引用類型失敗,dynamic_cast 運算符引發 bad_cast 異常。
try {
    Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);
}
catch (bad_cast b) {
    cout << "Caught: " << b.what();
}
           

運作時類型資訊 (RTTI)

typeid

  • typeid 運算符允許在運作時确定對象的類型
  • type_id 傳回一個 type_info 對象的引用
  • 如果想通過基類的指針獲得派生類的資料類型,基類必須帶有虛函數
  • 隻能擷取對象的實際類型

type_info

  • type_info 類描述編譯器在程式中生成的類型資訊。此類的對象可以有效存儲指向類型的名稱的指針。type_info 類還可存儲适合比較兩個類型是否相等或比較其排列順序的編碼值。類型的編碼規則和排列順序是未指定的,并且可能因程式而異。
  • 頭檔案:typeinfo
#include <iostream>
using namespace std;

class Flyable                       // 能飛的
{
public:
    virtual void takeoff() = 0;     // 起飛
    virtual void land() = 0;        // 降落
};
class Bird : public Flyable         // 鳥
{
public:
    void foraging() {...}           // 覓食
    virtual void takeoff() {...}
    virtual void land() {...}
    virtual ~Bird(){}
};
class Plane : public Flyable        // 飛機
{
public:
    void carry() {...}              // 運輸
    virtual void takeoff() {...}
    virtual void land() {...}
};

class type_info
{
public:
    const char* name() const;
    bool operator == (const type_info & rhs) const;
    bool operator != (const type_info & rhs) const;
    int before(const type_info & rhs) const;
    virtual ~type_info();
private:
    ...
};

void doSomething(Flyable *obj)                 // 做些事情
{
    obj->takeoff();

    cout << typeid(*obj).name() << endl;        // 輸出傳入對象類型("class Bird" or "class Plane")

    if(typeid(*obj) == typeid(Bird))            // 判斷對象類型
    {
        Bird *bird = dynamic_cast<Bird *>(obj); // 對象轉化
        bird->foraging();
    }

    obj->land();
}

int main(){
	Bird *b = new Bird();
	doSomething(b);
	delete b;
	b = nullptr;
	return 0;
}