之前面試被問到C++里static的作用是什么,但我卻只知道static在java里的作用是什么,於是就google了一下c++相關面試題,發現這個同學總結的很棒。
就記錄一下。
原文地址:C/C++面試知識總結
侵刪
--------------------------正文分割線------------------------------------
const
// 類 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();
作用
- 修飾變量,說明該變量不可以被改變;
- 修飾指針,分為指向常量的指針和指針常量;
- 常量引用,經常用於形參類型,即避免了拷貝,又避免了函數對值的修改;
- 修飾成員函數,說明該成員函數內不能修改成員變量。
---------------------我是筆記分割線------------------------------------------------------
*關於初始化列表:[轉載][C++]類構造函數初始化列表
*關於常成員函數:常成員函數不能更新類的成員變量,也不能調用該類中沒有用const修飾的成員函數,只能調用常成員函數。
*關於常量指針:[轉載][c++]C++中指針常量和常量指針的區別
*關於常量引用需要再學習!
---------------------筆記結束分割線-------------------------------------------------
Volatile
volatile int i = 10;
- volatile 關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素(操作系統、硬件、其它線程等)更改。
- volatile 關鍵字聲明的變量,每次訪問時都必須從內存中取出值(沒有被 volatile 修飾的變量,可能由於編譯器的優化,從 CPU 寄存器中取值)
- const 可以是 volatile (如只讀的狀態寄存器)
- 指針可以是 volatile
static
作用
- 修飾普通變量,修改變量的存儲區域和生命周期,使變量存儲在靜態區,在 main 函數運行前就分配了空間,如果有初始值就用初始值初始化它,如果沒有初始值系統用默認值初始化它。
- 修飾普通函數,表明函數的作用范圍,僅在定義該函數的文件內才能使用。在多人開發項目時,為了防止與他人命令函數重名,可以將函數定位為 static。
- 修飾成員變量,修飾成員變量使所有的對象只保存一個該變量,而且不需要生成對象就可以訪問該成員。
- 修飾成員函數,修飾成員函數使得不需要生成對象就可以訪問該函數,但是在 static 函數內不能訪問非靜態成員。
this 指針
this指針是一個隱含於每一個成員函數中的特殊指針。它指向正在被該成員函數操作的那個對象。- 當對一個對象調用成員函數時,編譯程序先將對象的地址賦給
this指針,然后調用成員函數,每次成員函數存取數據成員時,由隱含使用this指針。 - 當一個成員函數被調用時,自動向它傳遞一個隱含的參數,該參數是一個指向這個成員函數所在的對象的指針。
this指針被隱含地聲明為:ClassName *const this,這意味着不能給this指針賦值;在ClassName類的const成員函數中,this指針的類型為:const ClassName* const,這說明不能對this指針所指向的這種對象是不可修改的(即不能對這種對象的數據成員進行賦值操作);this並不是一個常規變量,而是個右值,所以不能取得this的地址(不能&this)。- 在以下場景中,經常需要顯式引用
this指針:- 為實現對象的鏈式引用;
- 為避免對同一對象進行賦值操作;
- 在實現一些數據結構時,如
list。
------------------------我是筆記分割線---------------------------
*關於左值,右值:[轉載][C++]C++11 左值、右值、右值引用詳解
--------------------------筆記結束分割線-------------------------
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,...) {/****/};
編譯器對inline函數的處理步驟
- 將 inline 函數體復制到 inline 函數調用點處;
- 為所用 inline 函數中的局部變量分配內存空間;
- 將 inline 函數的的輸入參數和返回值映射到調用方法的局部變量空間中;
- 如果 inline 函數有多個返回點,將其轉變為 inline 函數代碼塊末尾的分支(使用 GOTO)。
優缺點
優點
- 內聯函數同宏函數一樣將在被調用處進行代碼展開,省去了參數壓棧、棧幀開辟與回收,結果返回等,從而提高程序運行速度。
- 內聯函數相比宏函數來說,在代碼展開時,會做安全檢查或自動類型轉換(同普通函數),而宏定義則不會。
- 在類中聲明同時定義的成員函數,自動轉化為內聯函數,因此內聯函數可以訪問類的成員變量,宏定義則不能。
- 內聯函數在運行時可調試,而宏定義不可以。
缺點
- 代碼膨脹。內聯是以代碼膨脹(復制)為代價,消除函數調用帶來的開銷。如果執行函數體內代碼的時間,相比於函數調用的開銷較大,那么效率的收獲會很少。另一方面,每一處內聯函數的調用都要復制代碼,將使程序的總代碼量增大,消耗更多的內存空間。
- inline 函數無法隨着函數庫升級而升級。inline函數的改變需要重新編譯,不像 non-inline 可以直接鏈接。
- 是否內聯,程序員不可控。內聯函數只是對編譯器的建議,是否對函數內聯,決定權在於編譯器。
-----------------------------------筆記分割----------------------
*關於宏定義函數:[轉載][c++]用宏(Macro)定義一個函數
---------------------------------就這么點(⁎⁍̴̛ᴗ⁍̴̛⁎)------------------
虛函數(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; }
assert()
斷言,是宏,而非函數。assert 宏的原型定義在<assert.h>(C)、<cassert>(C++)中,其作用是如果它的條件返回錯誤,則終止程序執行。
如
assert( p != NULL );
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) // 恢復對齊狀態
------------------筆記分割線------------------------------------
關於字節對齊問題:仔細討論 C/C++ 字節對齊問題
-----------------------------------------------------------------
extern "C"
- 被 extern 限定的函數或變量是 extern 類型的
- 被
extern "C"修飾的變量和函數是按照 C 語言方式編譯和連接的
extern "C" 的作用是讓 C++ 編譯器將 extern "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 更適合看成是一個對象的實現體。
區別
- 最本質的一個區別就是默認的訪問控制
- 默認的繼承訪問權限。struct 是 public 的,class 是 private 的。
- struct 作為數據結構的實現體,它默認的數據訪問控制是 public 的,而 class 作為對象的實現體,它默認的成員變量訪問控制是 private 的。
C 實現 C++ 類
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 namespace_name::name
盡量不要使用 using namespace std; 污染命名空間
一般說來,使用 using 命令比使用 using 編譯命令更安全,這是由於它只導入了制定的名稱。如果該名稱與局部名稱發生沖突,編譯器將發出指示。using編譯命令導入所有的名稱,包括可能並不需要的名稱。如果與局部名稱發生沖突,則局部名稱將覆蓋名稱空間版本,而編譯器並不會發出警告。另外,名稱空間的開放性意味着名稱空間的名稱可能分散在多個地方,這使得難以准確知道添加了哪些名稱。
盡量不要使用
using namespace std;
應該使用
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;
:: 范圍解析運算符
:: 可以加在類型名稱(類、類成員、成員函數、變量等)前,表示作用域為全局命名空間
如
int count = 0; // global count int main() { int count = 0; // local count ::count = 1; // set global count to 1 count = 2; // set local count to 2 return 0; }
宏
- 宏定義可以實現類似於函數的功能,但是它終歸不是函數,而宏定義中括弧中的“參數”也不是真的參數,在宏展開的時候對 “參數” 進行的是一對一的替換。
初始化列表
好處
- 更高效:少了一次調用默認構造函數的過程。
- 有些場合必須要用初始化列表:
- 常量成員,因為常量只能初始化不能賦值,所以必須放在初始化列表里面
- 引用類型,引用必須在定義的時候初始化,並且不能重新賦值,所以也要寫在初始化列表里面
- 沒有默認構造函數的類類型,因為使用初始化列表可以不必調用默認構造函數來初始化,而是直接調用拷貝構造函數初始化。
面向對象
面向對象程序設計(Object-oriented programming,OOP)是種具有對象概念的程序編程典范,同時也是一種程序開發的抽象方針。
面向對象三大特征 —— 封裝、繼承、多態
封裝
- 把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
- 關鍵字: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)嗎?
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; }
抽象類、接口類、聚合類
- 抽象類:含有純虛函數的類
- 接口類:僅含有純虛函數的抽象類
- 聚合類:用戶可以直接訪問其成員,並且具有特殊的初始化語法形式。滿足如下特點:
- 所有成員都是 public
- 沒有有定於任何構造函數
- 沒有類內初始化
- 沒有基類,也沒有 virtual 函數
- 如:
// 定義 struct Date { int ival; string s; } // 初始化 Data vall = { 0, "Anna" };
虛函數、純虛函數
- 類里如果聲明了虛函數,這個函數是實現的,哪怕是空實現,它的作用就是為了能讓這個函數在它的子類里面可以被覆蓋,這樣的話,這樣編譯器就可以使用后期綁定來達到多態了。純虛函數只是一個接口,是個函數的聲明而已,它要留到子類里去實現。
- 虛函數在子類里面也可以不重載的;但純虛函數必須在子類去實現。
- 虛函數的類用於 “實作繼承”,繼承接口的同時也繼承了父類的實現。當然大家也可以完成自己的實現。純虛函數關注的是接口的統一性,實現由子類完成。
- 帶純虛函數的類叫虛基類,這種基類不能直接生成對象,而只有被繼承,並重寫其虛函數后,才能使用。這樣的類也叫抽象類。抽象類和大家口頭常說的虛基類還是有區別的,在 C# 中用 abstract 定義抽象類,而在 C++ 中有抽象類的概念,但是沒有這個關鍵字。抽象類被繼承后,子類可以繼續是抽象類,也可以是普通類,而虛基類,是含有純虛函數的類,它如果被繼承,那么子類就必須實現虛基類里面的所有純虛函數,其子類不能是抽象類。
虛函數指針、虛函數表
- 虛函數指針:在含有虛函數類的對象中,指向虛函數表,在運行時確定。
- 虛函數表:在程序只讀數據段(
.rodata section,見:目標文件存儲結構),存放虛函數指針,如果派生類實現了基類的某個虛函數,則在虛表中覆蓋原本基類的那個虛函數指針,在編譯時根據類的聲明創建。
虛繼承、虛函數
虛繼承
虛繼承用於解決多繼承條件下的菱形繼承問題(浪費存儲空間、存在二義性)。
底層實現原理與編譯器相關,一般通過虛基類指針和虛基類表實現,每個虛繼承的子類都有一個虛基類指針(占用一個指針的存儲空間,4字節)和虛基類表(不占用類對象的存儲空間)(需要強調的是,虛基類依舊會在子類里面存在拷貝,只是僅僅最多存在一份而已,並不是不在子類里面了);當虛繼承的子類被當做父類繼承時,虛基類指針也會被繼承。
實際上,vbptr 指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持着公共基類(虛基類)的兩份同樣的拷貝,節省了存儲空間。
虛繼承與虛函數
- 相同之處:都利用了虛指針(均占用類的存儲空間)和虛表(均不占用類的存儲空間)
- 不同之處:
- 虛繼承
- 虛基類依舊存在繼承類中,只占用存儲空間
- 虛基類表存儲的是虛基類相對直接繼承類的偏移
- 虛函數
- 虛函數不占用存儲空間
- 虛函數表存儲的是虛函數地址
- 虛繼承
內存分配和管理
malloc、calloc、realloc、alloca
- malloc:申請指定字節數的內存。申請到的內存中的初始值不確定。
- calloc:為指定長度的對象,分配能容納其指定個數的內存。申請到的內存的每一位(bit)都初始化為 0。
- realloc:更改以前分配的內存長度(增加或減少)。當增加長度時,可能需將以前分配區的內容移到另一個足夠大的區域,而新增區域內的初始值則不確定。
- alloca:在棧上申請內存。程序在出棧的時候,會自動釋放內存。但是需要注意的是,alloca 不具可移植性, 而且在沒有傳統堆棧的機器上很難實現。alloca 不宜使用在必須廣泛移植的程序中。C99 中支持變長數組 (VLA),可以用來替代 alloca。
malloc、free
申請內存,確認是否申請成功
char *str = (char*) malloc(100); assert(str != nullptr);
釋放內存后指針置空
free(p); p = nullptr;
new、delete
- new/new[]:完成兩件事,先底層調用 malloc 分了配內存,然后調用構造函數(創建對象)。
- delete/delete[]:也完成兩件事,先調用析構函數(清理資源),然后底層調用 free 釋放空間。
- new 在申請內存時會自動計算所需字節數,而 malloc 則需我們自己輸入申請內存空間的字節數。
int main() { T* t = new T(); // 先內存分配 ,再構造函數 delete t; // 先析構函數,再內存釋放 return 0; }
delete this 合法嗎?
Is it legal (and moral) for a member function to say 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
- shared_ptr
- unique_ptr
- weak_ptr
- 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 ——特別有用。
shared_ptr
多個智能指針可以共享同一個對象,對象的最末一個擁有着有責任銷毀對象,並清理與該對象相關的所有資源。
- 支持定制型刪除器(custom deleter),可防范 Cross-DLL 問題(對象在動態鏈接庫(DLL)中被 new 創建,卻在另一個 DLL 內被 delete 銷毀)、自動解除互斥鎖
weak_ptr
weak_ptr 允許你共享但不擁有某對象,一旦最末一個擁有該對象的智能指針失去了所有權,任何 weak_ptr 都會自動成空(empty)。因此,在 default 和 copy 構造函數之外,weak_ptr 只提供 “接受一個 shared_ptr” 的構造函數。
- 可打破環狀引用(cycles of references,兩個其實已經沒有被使用的對象彼此互指,使之看似還在 “被使用” 的狀態)的問題
unique_ptr
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)
dynamic_cast
- 用於多態類型的轉換
typeid
- typeid 運算符允許在運行時確定對象的類型
- type_id 返回一個 type_info 對象的引用
- 如果想通過基類的指針獲得派生類的數據類型,基類必須帶有虛函數
- 只能獲取對象的實際類型
type_info
- type_info 類描述編譯器在程序中生成的類型信息。 此類的對象可以有效存儲指向類型的名稱的指針。 type_info 類還可存儲適合比較兩個類型是否相等或比較其排列順序的編碼值。 類型的編碼規則和排列順序是未指定的,並且可能因程序而異。
- 頭文件:
typeinfo
typeid、type_info 使用
class Flyable // 能飛的 { public: virtual void takeoff() = 0; // 起飛 virtual void land() = 0; // 降落 }; class Bird : public Flyable // 鳥 { public: void foraging() {...} // 覓食 virtual void takeoff() {...} virtual void land() {...} }; class Plane : public Flyable // 飛機 { public: void carry() {...} // 運輸 virtual void take off() {...} 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: ... }; class 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(); };
Effective C++
- 視 C++ 為一個語言聯邦(C、Object-Oriented C++、Template C++、STL)
- 盡量以
const、enum、inline替換#define(寧可以編譯器替換預處理器) - 盡可能使用 const
- 確定對象被使用前已先被初始化(構造時賦值(copy 構造函數)比 default 構造后賦值(copy assignment)效率高)
- 了解 C++ 默默編寫並調用哪些函數(編譯器暗自為 class 創建 default 構造函數、copy 構造函數、copy assignment 操作符、析構函數)
- 若不想使用編譯器自動生成的函數,就應該明確拒絕(將不想使用的成員函數聲明為 private,並且不予實現)
- 為多態基類聲明 virtual 析構函數(如果 class 帶有任何 virtual 函數,它就應該擁有一個 virtual 析構函數)
- 別讓異常逃離析構函數(析構函數應該吞下不傳播異常,或者結束程序,而不是吐出異常;如果要處理異常應該在非析構的普通函數處理)
- 絕不在構造和析構過程中調用 virtual 函數(因為這類調用從不下降至 derived class)
- 令
operator=返回一個reference to *this(用於連鎖賦值) - 在
operator=中處理 “自我賦值” - 賦值對象時應確保復制 “對象內的所有成員變量” 及 “所有 base class 成分”(調用基類復制構造函數)
- 以對象管理資源(資源在構造函數獲得,在析構函數釋放,建議使用智能指針,資源取得時機便是初始化時機(Resource Acquisition Is Initialization,RAII))
- 在資源管理類中小心 copying 行為(普遍的 RAII class copying 行為是:抑制 copying、引用計數、深度拷貝、轉移底部資源擁有權(類似 auto_ptr))
- 在資源管理類中提供對原始資源(raw resources)的訪問(對原始資源的訪問可能經過顯式轉換或隱式轉換,一般而言顯示轉換比較安全,隱式轉換對客戶比較方便)
- 成對使用 new 和 delete 時要采取相同形式(
new中使用[]則delete [],new中不使用[]則delete) - 以獨立語句將 newed 對象存儲於(置入)智能指針(如果不這樣做,可能會因為編譯器優化,導致難以察覺的資源泄漏)
- 讓接口容易被正確使用,不易被誤用(促進正常使用的辦法:接口的一致性、內置類型的行為兼容;阻止誤用的辦法:建立新類型,限制類型上的操作,約束對象值、消除客戶的資源管理責任)
- 設計 class 猶如設計 type,需要考慮對象創建、銷毀、初始化、賦值、值傳遞、合法值、繼承關系、轉換、一般化等等。
- 寧以 pass-by-reference-to-const 替換 pass-by-value (前者通常更高效、避免切割問題(slicing problem),但不適用於內置類型、STL迭代器、函數對象)
- 必須返回對象時,別妄想返回其 reference(絕不返回 pointer 或 reference 指向一個 local stack 對象,或返回 reference 指向一個 heap-allocated 對象,或返回 pointer 或 reference 指向一個 local static 對象而有可能同時需要多個這樣的對象。)
- 將成員變量聲明為 private(為了封裝、一致性、對其讀寫精確控制等)
- 寧以 non-member、non-friend 替換 member 函數(可增加封裝性、包裹彈性(packaging flexibility)、機能擴充性)
- 若所有參數(包括被this指針所指的那個隱喻參數)皆須要類型轉換,請為此采用 non-member 函數
- 考慮寫一個不拋異常的 swap 函數
- 盡可能延后變量定義式的出現時間(可增加程序清晰度並改善程序效率)
- 盡量少做轉型動作(舊式:
(T)expression、T(expression);新式:const_cast<T>(expression)、dynamic_cast<T>(expression)、reinterpret_cast<T>(expression)、static_cast<T>(expression)、;盡量避免轉型、注重效率避免 dynamic_casts、盡量設計成無需轉型、可把轉型封裝成函數、寧可用新式轉型) - 避免使用 handles(包括 引用、指針、迭代器)指向對象內部(以增加封裝性、使 const 成員函數的行為更像 const、降低 “虛吊號碼牌”(dangling handles,如懸空指針等)的可能性)


