一些C 概念辨析

2022-06-05 12:30:07 字數 3953 閱讀 8207

const與指標的結合方式有時候令人迷惑,如:

int * a0;

int * const a1;

int const * a2;

const int * a3;

int const * const a4;

const int * const a5;

const int const * const a6;

相比於乙個裸指標 a0,const 限定了指標的可變性,分為兩層:頂層指標的可變性、底層資料的可變性。

頂層指標的可變性,指的是指標本身的指向是否可變,是否能從指向乙個元素變成指向另乙個元素,比如是否能用該型別的指標遍歷乙個陣列。

底層資料的可變性,指的是指標指向的資料是否可變,是否能從乙個值原地改變成另乙個值,比如常用的qsort傳入的cmp函式就限制了不能修改底層資料。

區分const限制的是頂層指標還是底層資料,要看const相對於*的位置。*將乙個基礎型別的裸指標分為兩部分,左邊描述了底層資料的型別,右邊描述了這個裸指標的名字。如果const靠近左邊底層資料型別,則限制的是底層資料不可變;如果const靠近右邊頂層指標,則限制的是頂層指標不可變。

所以,a1 是頂層指標不可變,a2 和 a3 是底層資料不可變,a4、a5 和 a6 是指標資料都不可變。當然,最常用的還是 a3 這種形式,用於qsort函式的引數。

c++裡的引用相當於不可變的頂層指標,c++裡的const引用,則相當於指標資料都不可邊的指標,廣泛用於 sort 和 priority_queue。

也就是說:

int b;

int &a = b; // eq. int * const pa = &b; a = *pa;

const int &a = b; // eq. const int * const pa = &b; a = *pa;

但是引用有很多優點:寫法簡單、省掉一次解引用、不會發生解空指標、不會有野指標……

class a 

void fun(int a)

virtual double fun(double x, double y)

virtual void fun(const char* str)

static void fun(char);

};void a::fun(char c)

class b : public a

virtual double fun(double x, double y) override

};int main() ;

return 0;

}

可以看到,overload 是同一層級內部的水平關係,override 和 hide 是不同層級之間的垂直關係。

c++ 中,利用同型別的乙個物件來構造另乙個物件,有兩種不同的形成方式:賦值和拷貝。乙個簡答的例子如下:

class a 

a& operator= (const a& a)

};int main()

在這裡例子中,a1是呼叫的拷貝建構函式,直接憑空生成乙個新物件;a2呼叫了賦值操作符,在已有物件的基礎上進行賦值,修改了原有的物件。

class t                  // 預設建構函式,無參

explicit t (int r) : t (r, 0) {} // 轉換建構函式,單參,且引數是其他型別

t (int a, int b) : x(a), y(b) {} // 初始化建構函式,有參

t (const t&); // 拷貝建構函式

t (t&&); // 移動建構函式

t& operator= (const t&); // 拷貝賦值

t& operator= (t&&); // 移動賦值

}

單個引數的建構函式稱為轉換建構函式,使用explicit修飾之後,可以禁止隱式型別轉換,只允許以顯式轉換構造。建議的做法是總是使用 explicit,這樣,t t = 10;之類的語句就無法通過編譯了。

拷貝和賦值的區別前面已經提到過,這裡另外介紹兩個關鍵字defaultdelete。當不指定建構函式的時候,編譯器預設提供無參建構函式,以及淺拷貝的拷貝建構函式。當給定了建構函式,編譯器就不再提供預設建構函式,但是可以通過使用t() = default;來啟用編譯器提供的建構函式。與之相對的,可以通過t& operator= (const t&) = delete;來取消編譯器提供的賦值操作。典型應用場景是unique_ptr以及單例。

委託建構函式是另一種特例。早期的時候,每個建構函式都需要手動初始化類的每乙個成員,無法一次性指定每個成員的預設值。委託建構函式的寫法是,使用乙個建構函式寫好所有的構造邏輯,其他建構函式只需要呼叫這個建構函式,達到委託的效果。上面的例子中,單參和無參的建構函式,就是委託了雙參建構函式來完成工作。

移動構造,是配合右值引用來實現的。主要使用場景是物件的「搬運」,也就是利用乙個將要消失的物件,來建立乙個仍要使用的物件。可用於代替rvo。

c++17引入了結構化繫結(structured binding),這種好東西一方面為我們簡化**帶來了方便,比如不用寫paira.first這種;另一方面與其他特性的結合有時會有一些意想不到的情況發生。

比如下面這個bfs的例子,型別推導 + 右值引用 + 結構化繫結會導致記憶體出錯:

queue> q;

q.push();

while (!q.empty())

問題出在auto&& [i, j] = q.front(); q.pop();這兩句上,我們使用的是乙個引用,但是後來這個元素被 pop 釋放掉了,所以會報記憶體錯誤。

使用auto& [i, j]也會出錯,因為也是引用型別;只有使用auto [i, j]做拷貝賦值才不會引起記憶體錯誤。

什麼時候用auto&&是安全的呢?在變數被使用前不會釋放記憶體的時候,比如for (auto&& x : vec);或者物件的所有權被直接轉移給右值引用的時候auto&& res = func();

除了結構化繫結(structured binding),可以將乙個結構體拆解開分別賦值之外;c++還加入了一堆 emplace/emplace_back/emplace_front 函式,分別對應於 push/push_back/push_front 系列函式,與之不同的是不適用拷貝賦值的方式,而是直接在容器內「原地構造」省掉了臨時物件的開銷,基本相當於 push + move 操作。

什麼物件才能用結構化繫結和原地構造呢?

答案是可以準確知道結構的型別,比如 struct、pair、tuple 等,而 vector 這一類不定長的則不能使用結構化繫結來獲取,也不能適用emplace來存入。如以下寫法是錯誤的:

vector> res; res.emplace_back(2, 3); // can't get  } but get  } for it calls vector(n, val)

auto [start, end] = res[0]; // ce, can't decompose vector with structured binding

與結構化繫結類似,原地構造也是需要知道物件的記憶體布局,才能呼叫 統一初始化函式,不然可能呼叫的某個建構函式。比如上面的例子,vector的單參和雙參建構函式都有其特殊含義,和統一初始化的結構並不一致。

C 一些概念理解

封裝 隱藏細節,資料和方法實行public,private,protece 繼承 不修改的前提下擴充套件功能 多型 將父類設定成於子類對等地執行操作 過載是函式名相同,引數不同 重寫是函式名相同,引數相同,子類重新定義父類的虛函式 1 類中有const和引用型別的成員。2 類中有某個成員類沒有pub...

C 一些基本概念

建構函式的作用是對物件本身做初始化工作,也就是給使用者提供初始化類中成員變數的一種方式。析構函式是釋放物件執行期間所申請的資源。函式的過載,過載構成的條件 函式的引數型別不同 引數個數不同,才能構成函式的過載 在乙個類中 注意,只有函式的返回型別不同是不能構成函式的過載。在函式過載時,要注意函式帶有...

C 的一些基礎概念

cpp中有預編譯指令 include其中iostream提供乙個命名空間的東西,標準命名空間是std c 中輸入輸出不能直接寫出以下形式 cin a cout a endl 別忘了要事先宣告命名空間中的變數!方式一 std cin a std cout a std endl using std co...