1 static關鍵字
加了 static 關鍵字的全局變量只能在本文件中使用。
static 定義的靜態局部變量分配在數據段上,普通的局部變量分配在棧上,會因為函數棧幀的釋放而被釋放掉。
1.1 全局靜態變量
在全局變量前加上關鍵字 static,全局變量就定義成一個 全局靜態變量 。
內存中的位置: 靜態存儲區 ,在整個程序運行期間一直存在。
初始化:未經初始化的全局靜態變量會被 自動初始化為 0 (自動對象的值是任意的,除非他被顯式初始化);
作用域:全局靜態變量在聲明 僅在本文件可見 ,他的文件之外是不可見的,準確地說是從定義之處開始,到文件結尾。
1.2 局部靜態變量
在局部變量之前加上關鍵字 static,局部變量就成為一個 局部靜態變量 。
內存中的位置: 靜態存儲區 ,在整個程序運行期間一直存在。
初始化:未經初始化的全局靜態變量會被自動初始化為 0(自動對象的值是任意的,除非他被顯式初始化);
作用域:作用域仍為局部作用域,當定義它的函數或者語句塊結束的時候,作用域結束。但是當局部靜態變量離開作用域后,并沒有銷毀,而是 仍然駐留在內存當中 ,只不過我們不能再對它進行訪問,直到該函數再次被調用,并且值不變;
1.3 靜態函數
在函數返回類型前加 static,函數就定義為 靜態函數 。函數的定義和聲明在默認情況下都是 extern 的,但靜態函數 僅在本文件可見 ,不能被其他文件所用。
函數的實現使用 static 修飾,那么這個函數只可在本 cpp 內使用,不會同其他 cpp 中的同名函數引起沖突;
warning:在 頭文件中聲明非static 的全局函數 ,在 cpp 內聲明static 的全局函數 ,如果你要在多個 cpp 中復用該函數,就把它的聲明提到頭文件里去,否則 cpp 內部聲明需加上 static 修飾;
1.4 類的靜態成員
對一個類中成員變量和成員函數來說,加了 static 關鍵字,則此變量/函數就沒有 this指針了,必須通過 類名訪問 。
在類中,靜態成員可以實現多個對象之間的數據共享,并且使用靜態數據成員還不會破壞隱藏的原則,即保證了安全性。因此, 靜態成員是類的所有對象***享的成員 ,而不是某個對象的成員。對多個對象來說,靜態數據成員 只存儲一處 ,供所有對象共用。
1.5 類的靜態函數
靜態成員函數和靜態數據成員一樣,它們都屬于 類的靜態成員 ,它們都不是對象成員。因此,對靜態成員的引用不需要用對象名。在 靜態成員函數的實現中不能直接引用類中說明的非靜態成員 ,可以引用類中說明的靜態成員(這點非常重要)。如果靜態成員函數中要引用非靜態成員時,可通過對象來引用。從中可看出,調用靜態成員函數使用如下格式:<類名>::<靜態成員函數名>(<參數表>);
2 C++和C的區別
2.1 設計思想上
C++是面向對象的語言,而 C 是面向過程的結構化編程語言
2.2 語法上
C++具有 重載 、 繼承 、多態三種特性;
C++相比 C,增加多許多類型安全的功能,比如強制類型轉換;
C++支持 范式編程 ,比如模板類、函數模板等。
3 C++中四種cast轉換
C++中四種類型轉換是:static_cast, dynamic_cast, const_cast, reinterpret_cast
3.1 const_cast
用于將 const 變量轉為 非 const 。它也是四個強制類型轉換運算符中唯一能夠去除 const 屬性的運算符。對于未定義 const 版本的成員函數,我們通常需要使用 const_cast 來去除 const引用對象的 const,完成函數調用。另外一種使用方式,結合 static_cast,可以在非 const 版本的成員函數內添加 const,調用完 const 版本的成員函數后,再使用 const_cast 去除 const限定。
3.2 static_cast
static_cast< new_type >(expression)
// new_type 為目標數據類型,expression 為原始數據類型變量或者表達式。
基本數據類型之間的轉換,如int、float、char之間的互相轉換;用于各種 隱式轉換 ,比如非 const 轉 const,void*轉指針等,但 沒有運行時類型檢查來保證轉換的安全性 。
隱式類型轉換 :首先,對于內置類型,低精度的變量給高精度變量賦值會發生隱式類型轉換,其次,對于只存在單個參數的構造函數的對象構造來說,函數調用可以直接使用該參數傳入,編譯器會自動調用其構造函數生成 臨時對象 。
static_cast主要有如下幾種用法:
-
用于類層次結構中基類和派生類之間指針或引用的轉換。
進行向上轉換是安全的;
進行向下轉換時,由于沒有動態類型檢查,所以是不安全的。因為 基類不包含派生類的成員變量,無法對派生類的成員變量賦值。 -
用于基本數據類型之間的轉換,如int、float、char之間的互相轉換
-
把空指針轉換成 目標類型的空指針 。
-
把任何類型的表達式轉 換成void類型 。
注意:static_cast不能去掉expression的const、volatile、或者__unaligned屬性。
char a = 'a'; int b = static_cast<char>(a); //將char型數據轉換成int型數據
const int g = 20;
int *h = static_cast<int*>(&g); //編譯錯誤,static_cast不能去掉g的const屬性
class Base
{};
class Derived : public Base
{}
Base* pB = new Base();
if(Derived* pD = static_cast(pB))
{} //下行轉換是不安全的(堅決抵制這種方法)
Derived* pD = new Derived();
if(Base* pB = static_cast (pD))
{} //上行轉換是安全的
3.3 dynamic_cast
dynamic_cast< new_type >(expression)
// new_type 為目標數據類型,expression 為原始數據類型變量或者表達式。
dynamic_cast< type* >(e) //type必須是一個類類型且必須是一個有效的指針
dynamic_cast< type& >(e) //type必須是一個類類型且必須是一個左值
dynamic_cast< type&& >(e) //type必須是一個類類型且必須是一個右值
用于 動態類型轉換 。只能用于 含有虛函數的類 ,用于類層次間的向上和向下轉化、類之間的 交叉轉換 (cross cast)。只能轉指針或 引用 。
在類層次間向上轉換時,dynamic_cast和static_cast的效果是一樣的;在進行向下轉換時,dynamic_cast具有類型檢查的功能,它通過判斷在執行到該語句的時候,變量類型和要轉換的類型是否相同來判斷是否能夠進行向下轉換,如果是非法的對于轉換目標是指針類型返回 NULL,對于引用拋std::bad_cast異常比static_cast更安全。
3.4 reinterpret_cast
幾乎什么都可以轉,比如將 int 轉指針,執行的是逐個比特復制的操作。容易出問題,盡量少用。
3.5 為何不用C的強制轉換
C 的強制轉換表面上看起來功能強大什么都能轉,但是轉化不夠明確,不能進行錯誤檢查,容易出錯。
4 C/C++中指針和引用的區別
4.1 指針
指針利用地址,它的值直接指向存在電腦存儲器中另一個地方的值。由于通過地址能找到所需的變量單元,可以說,地址指向該變量單元。因此,將地址形象化的稱為“指針”。意思是通過它能找到以它為地址的內存單元。
4.2 引用
引用就是某一變量的一個 別名 ,對引用的操作與對變量直接操作完全一樣。引用的聲明方法:類型標識符 &引用名=目標變量名;引用引入了對象的一個同義詞。定義引用的表示方法與定義指針相似,只是用&代替了*
4.3 區別
- 指針有自己的一塊空間,而引用只是一個別名;
- 使用 sizeof 看一個指針的大小是 4,而引用則是被引用對象的大小;
- 指針可以被初始化為 NULL,而引用必須被初始化且必須是一個已有對象的引用;
- 作為參數傳遞時,指針需要被解引用才可以對對象進行操作,而對引用的修改都會改變引用所指向的對象;
- 可以有 const 指針,但是沒有 const 引用;
- 指針在使用中可以指向其它對象,但是引用只能是一個對象的引用,不能 被改變;
- 指針可以有多級指針(**p),而引用至于一級;
- 指針和引用使用++運算符的意義不一樣;
- 如果返回動態內存分配的對象或者內存,必須使用指針,引用可能引起內存泄露。
5 C++智能指針
C++里面的四個智能指針: auto_ptr , shared_ptr , weak_ptr , unique_ptr 其中后三個是c++11 支持,并且第一個已經被 11 棄用。
為什么要使用智能指針:
智能指針的作用是 管理一個指針 ,因為存在以下這種情況:申請的空間在函數結束時 忘記釋放 ,造成 內存泄漏 。使用智能指針可以很大程度上的避免這個問題,因為智能指針就是一個 類 ,當超出了類的作用域是,類會 自動調用析構函數 ,析構函數會自動釋放資源。所以智能指針的作用原理就是在函數結束時自動釋放內存空間,不需要手動釋放內存空間。
對 shared_ptr 進行初始化時不能將一個普通指針直接賦值給智能指針,因為一個是指針,一個是類。可以通過 make_shared 函數或者通過構造函數傳入普通指針。并可以通過 get 函數獲得普通指針。
5.1 auto_ptr
c++98 的方案,cpp11 已經拋棄。
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr p2;
p2 = p1; //auto_ptr 不會報錯.
此時不會報錯, p2 剝奪了 p1 的所有權 ,但是當程序運行時訪問 p1 將會報錯。所以 auto_ptr存在潛在的內存***問題。
5.2 unique_ptr
替換 auto_ptr。unique_ptr 實現獨占式擁有或嚴格擁有概念,保證同一時間內只有一個智能指針可以指向該對象。它對于避免資源泄露(例如:以 new 創建對象后因為發生異常而忘記調用 delete)特別有用。還是上面那個例子:
unique_ptr<string> p3 (new string ("auto")); //#4
unique_ptr<string> p4; //#5
p4 = p3; //此時會報錯!!
編譯器認為 p4=p3 非法,避免了 p3 不再指向有效數據的問題。因此,unique_ptr 比 auto_ptr更安全。另外unique_ptr 還有更聰明的地方:當程序試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這么做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這么做,比如:
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1 留下懸掛的 unique_ptr(pu1),這可能導致危害。而#2 不會留下懸掛的 unique_ptr,因為它調用unique_ptr 的構造函數,該構造函數創建的臨時對象在其所有權讓給 pu3 后就會被銷毀。這種隨情況而已的行為表明,unique_ptr 優于允許兩種賦值的 auto_ptr 。
注:如果確實想執行類似與#1 的操作,要安全的重用這種指針,可給它賦新值。C++有一個標準庫函數 std::move(),讓你能夠將一個 unique_ptr 賦給另一個。例如:
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
5.3 shared_ptr
shared_ptr 實現共享式擁有概念。多個智能指針可以指向相同對象,該對象和其相關資源會在“最后一個引用被銷毀”時候釋放。從名字 share 就可以看出了資源可以被多個指針共享,它使用計數機制來表明資源被幾個指針共享。可以通過成員函數 use_count()來查看資源的所有者個數。除了可以通過 new 來構造,還可以通過傳入auto_ptr, unique_ptr,weak_ptr 來構造。當我們調用 release()時,當前指針會釋放資源所有權,計數減一。當計數等于 0 時,資源會被釋放。
shared_ptr 是為了解決 auto_ptr 在對象所有權上的局限性(auto_ptr 是獨占的), 在使用引用計數的機制上提供了可以共享所有權的智能指針。
成員函數:
- use_count 返回引用計數的個數
- unique 返回是否是獨占所有權( use_count 為 1)
- swap 交換兩個 shared_ptr 對象(即交換所擁有的對象)
- reset 放棄內部對象的所有權或擁有對象的變更, 會引起原有對象的引用計數的減少
- get 返回內部對象(指針), 由于已經重載了()方法, 因此和直接使用對象是一樣的。如:
shared_ptr<int> sp(new int(1)); // sp 與 sp.get()是等價的
5.4 weak_ptr
weak_ptr 是一種不控制對象生命周期的智能指針, 它指向一個 shared_ptr 管理的對象。weak_ptr 設計的目的是為協助shared_ptr 而引入的一種智能指針,它只可以從一個 shared_ptr 或另一個 weak_ptr 對象構造,它的構造和析構不會引起引用記數的增加或減少。
weak_ptr 是用來 解決 shared_ptr 相互引用時的死鎖問題 ,如果說兩個 shared_ptr 相互引用,那么這兩個指針的引用計數永遠不可能下降為 0,資源永遠不會釋放。它是對對象的一種弱引用,不會增加對象的引用計數,和 shared_ptr 之間可以相互轉化,shared_ptr 可以直接賦值給它,它可以通過調用 lock 函數來獲得 shared_ptr。
class B;
class A
{
public:
shared_ptr pb_;
~A()
{
cout<<"A delete\\n";
}
};
class B
{
public:
shared_ptr pa_;
~B()
{
cout<<"B delete\\n";
}
};
void fun()
{
shared_ptr pb(new B());
shared_ptr pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<endl;
cout<endl;
}
int main()
{
fun();
return 0;
}
可以看到 fun 函數中 pa,pb 之間互相引用,兩個資源的引用計數為 2,當要跳出函數時,智能指針 pa,pb 析構時兩個資源引用計數會減一,但是兩者引用計數還是為 1,導致跳出函數時資源沒有被釋放(A B 的析構函數沒有被調用),如果把其中一個改為 weak_ptr 就可以了,我們把類 A 里面的 shared_ptr pb
改為 weak_ptr pb
運行結果如下,這樣的話,資源 B 的引用開始就只有 1,當 pb 析構時,B 的計數變為 0,B 得到釋放,B 釋放的同時也會使 A 的計數減一,同時 pa 析構時使 A 的計數減一,那么 A 的計數為 0,A 得到釋放。
注意的是我們不能通過 weak_ptr 直接訪問對象的方法,比如 B 對象中有一個方法 print(),我們不能這樣訪問,pa->pb->print();
英文 pb_是一個 weak_ptr,應該先把它轉化為shared_ptr,如:
shared_ptr p = pa->pb_.lock();
p->print();
5.5 內存泄露
當兩個對象相互使用一個 shared_ptr 成員變量指向對方,會造成 循環引用 ,使引用計數失效,從而導致內存泄漏。
#include
#include
using namespace std;
class B;
class A
{
public: // 為了省去一些步驟這里 數據成員也聲明為public
shared_ptr pb;
~A()
{
cout << "kill A\\n";
}
};
class B
{
public:
shared_ptr pa;
~B()
{
cout <<"kill B\\n";
}
};
int main(int argc, char** argv)
{
shared_ptr sa(new A());
shared_ptr sb(new B());
if(sa && sb)
{
sa->pb=sb;
sb->pa=sa;
}
cout<<"sa use count:"<use_count()<return 0;
}
注意此時sa,sb都沒有釋放,產生了內存泄露問題。即A內部有指向B,B內部有指向A,這樣對于A,B必定是在A析構后B才析構,對于B,A必定是在B析構后才析構A,這就是循環引用問題,違反常規,導致內存泄露。
解決辦法
使用弱引用的智能指針weak_ptr打破這種循環引用。為了解決循環引用導致的內存泄漏,引入了weak_ptr 弱指針,weak_ptr 的構造函數不會修改引用計數的值,從而不會對對象的內存進行管理,其類似一個普通指針,但不指向引用計數的共享內存,但是其可以檢測到所管理的對象是否已經被釋放,從而避免非法訪問。
5.6 shared_ptr的實現
template <typename T>
class SmartPtr
{
private:
T *ptr; //底層真實的指針
int *use_count;//保存當前對象被多少指針引用計數
public:
SmartPtr(T *p); //SmartPtrp(new int(2));
SmartPtr(const SmartPtr&orig);//SmartPtrq(p);
SmartPtr&operator=(const SmartPtr &rhs);//q=p
~SmartPtr();
T operator*(); //為了能把智能指針當成普通指針操作定義解引用操作
T*operator->(); //定義取成員操作
T* operator+(int i);//定義指針加一個常數
int operator-(SmartPtr&t1, SmartPtr&t2);//定義兩個指針相減
void getcount()
{
return *use_count
}
};
template <typename T>
int SmartPtr::operator-(SmartPtr &t1, SmartPtr &t2)
{
return t1.ptr - t2.ptr;
}
template <typename T>
SmartPtr::SmartPtr(T *p)
{
ptr = p;
try
{
use_count = new int(1);
}
catch (...)
{
delete ptr; //申請失敗釋放真實指針和引用計數的內存
ptr = nullptr;
delete use_count;
use_count = nullptr;
}
}
template <typename T>
SmartPtr::SmartPtr(const SmartPtr &orig) //復制構造函數
{
use_count = orig.use_count;//引用計數保存在一塊內存,所有的 SmarPtr 對象的引用計數
都指向這里
this->ptr = orig.ptr;
++(*use_count); //當前對象的引用計數加 1
}
template <typename T>
SmartPtr& SmartPtr::operator=(const SmartPtr &rhs)
{
//重載=運算符,例如 SmartPtrp,q; p=q;這個語句中,首先給 q 指向的對象的引用計數加1,因為 p 重新指向了 q 所指的對象,所以 p 需要先給原來的對象的引用計數減 1,如果減一后為 0,先釋放掉 p 原來指向的內存,然后講 q 指向的對象的引用計數加 1 后賦值給 p
++*(rhs.use_count);
if ((--*(use_count)) == 0)
{
delete ptr;
ptr = nullptr;
delete use_count;
use_count = nullptr;
}
ptr = rhs.ptr;
*use_count = *(rhs.use_count);
return *this;
}
template <typename T>
SmartPtr::~SmartPtr()
{
getcount();
if (--(*use_count) == 0) //SmartPtr 的對象會在其生命周期結束的時候調用其析構函數,在析構函數中檢測當前對象的引用計數是不是只有正在結束生命周期的這個 SmartPtr 引用,如果是,就釋放掉,如果不是,就還有其他的 SmartPtr 引用當前對象,就等待其他的 SmartPtr對象在其生命周期結束的時候調用析構函數釋放掉
{
getcount();
delete ptr;
ptr = nullptr;
delete use_count;
use_count = nullptr;
}
}
template <typename T>
T SmartPtr::operator*()
{
return *ptr;
}
template <typename T>
T* SmartPtr::operator->()
{
return ptr;
}
template <typename T>
T* SmartPtr::operator+(int i)
{
T *temp = ptr + i;
return temp;
}
6 數組和指針
指針 | 數組 |
---|---|
保存數據的地址 | 保存數據 |
指針的內容為為地址,從該地址訪問數據 | 直接訪問數據 |
通常用于動態的數據結構 | 通常用于固定數目且數據類型相同的元素 |
通過 Malloc 分配內存,free 釋放內存 | 隱式的分配和刪除 |
通常指向匿名數據,操作匿名函數 | 自身即為數據名 |
7 野指針
野指針就是指向一個已刪除的對象或者未申請訪問受限內存區域的指針
8 函數指針
8.1 定義
函數指針是指向函數的指針變量。
函數指針本身首先是一個指針變量,該指針變量指向一個具體的函數。這正如用指針變量可指向整型變量、字符型、數組一樣,這里是指向函數。
C 在編譯時,每一個函數都有一個入口地址,該入口地址就是函數指針所指向的地址。有了指向函數的指針變量后,可用該指針變量調用函數,就如同用指針變量可引用其他類型變量一樣,在這些概念上是大體一致的。
8.2 用途:
調用函數和做函數的參數,比如回調函數。
8.3 示例:
char * fun(char * p) {…} // 函數 fun
char * (*pf)(char * p); // 函數指針 pf
pf = fun; // 函數指針 pf 指向函數 fun
pf(p); // 通過函數指針 pf 調用函數 fun
9 fork函數
Fork:創建一個和當前進程映像一樣的進程可以通過 fork( )系統調用:
#include
#include
pid_t fork(void);
成功調用 fork( )會 創建一個新的進程 ,它幾乎與調用 fork( )的進程一模一樣,這兩個進程都會繼續運行。在子進程中,成功的 fork( )調用會返回 0。在父進程中 fork( )返回子進程的 pid。如果出現錯誤,fork( )返回一個負值。
最常見的 fork( )用法是創建一個新的進程,然后使用 **exec( )**載入二進制映像,替換當前進程的映像。這種情況下,派生(fork)了新的進程,而這個子進程會執行一個新的二進制可執行文件的映像。這種“派生加執行”的方式是很常見的。
在早期的 Unix 系統中,創建進程比較原始。當調用 fork 時,內核會把所有的內部數據結構復制一份,復制進程的頁表項,然后把父進程的地址空間中的內容逐頁的復制到子進程的地址空間中。但從內核角度來說,逐頁的復制方式是十分耗時的。現代的 Unix 系統采取了更多的優化,例如 Linux,采用了寫時復制的方法,而不是對父進程空間進程整體復制。
10 析構函數
析構函數與構造函數對應,當對象結束其生命周期,如對象所在的函數已調用完畢時,系統會自動執行析構函數。
析構函數名也應與類名相同,只是在函數名前面加一個位取反符~,例如~stud( )
,以區別于構造函數。它 不能帶任何參數,也沒有返回值 (包括 void 類型)。只能有一個析構函數, 不能重載 。
如果用戶沒有編寫析構函數,編譯系統會自動生成一個缺省的析構函數(即使自定義了析構函數,編譯器也總是會為我們合成一個析構函數,并且如果自定義了析構函數,編譯器在執行時會先調用自定義的析構函數再調用合成的析構函數),它也不進行任何操作。所以許多簡單的類中沒有用顯式的析構函數。
如果一個類中有指針,且在使用的過程中動態的申請了內存,那么最好顯示構造析構函數在銷毀類之前,釋放掉申請的內存空間,避免內存泄漏。
10.1 類析構順序
- 派生類本身的析構函數
- 對象成員析構函數
- 基類析構函數
因為析構函數沒有參數,所以包含成員對象的類的析構函數形式上并無特殊之處。但在撤銷該類對象的時候,會首先調用自己的析構函數,再調用成員對象的析構函數,調用次序與初始化時的次序相反。
11 虛函數和多態
多態的實現主要分為靜態多態和 動態多態 , 靜態多態主要是重載 ,在 編譯的時候就已經確定 ;動態多態是用虛函數機制實現的,在 運行期間動態綁定 。例如:一個父類類型的指針指向一個子類對象時候,使用父類的指針去調用子類中重寫了的父類中的虛函數的時候,會調用子類重寫過后的函數,在父類中聲明為加了 virtual 關鍵字的函數,在子類中重寫時候不需要加 virtual也是虛函數。
虛函數的實現:在有虛函數的類中,類的最開始部分是一個虛函數表的 指針 ,這個指針指向一個 虛函數表 ,表中放了虛函數的地址,實際的虛函數在代碼段(.text)中。當子類繼承了父類的時候也會繼承其虛函數表,當子類重寫父類中虛函數時候,會將其繼承到的 虛函數表中的地址替換為重新寫的函數地址 。使用了虛函數,會增加訪問內存開銷,降低效率。
12 析構函數與虛函數
析構函數必須是虛函數,因為將可能會被繼承的父類的析構函數設置為虛函數,可以保證當我們 new 一個子類,然后使用基類指針指向該子類對象, 釋放基類指針時可以釋放掉子類的空間 ,防止內存泄漏。
C++默認的析構函數不是虛函數是因為虛函數需要額外的虛函數表和虛表指針,占用額外的內存。而對于不會被繼承的類來說,其析構函數如果是虛函數,就會浪費內存。因此 C++默認的析構函數不是虛函數,而是只有當需要當作父類時, 設置虛函數 。
13 靜態函數和虛函數
靜態函數在編譯的時候就已經確定運行時機,虛函數在運行的時候動態綁定。虛函數因為用了虛函數表機制,調用的時候會增加一次內存開銷。
13.1 靜態函數
用static修飾的函數,限定在本源碼文件中使用,不能被本源碼文件以外的代碼文件調用。 普通的函數,默認是extern的,也就是說,可以被其它代碼文件調用該函數。
13.2 虛函數表
當一個類中包含被virtual 關鍵字修飾的成員函數時,該成員函數就成為了一個 虛函數 。頭一個含有虛函數的類所實例化出來的對象都擁有同一個 虛函數表 ,在對象中含有一個* 虛函數指針 _vptr ,該指針指向該類的虛函數表,虛函數表保存的是類中虛函數的地址(一個類可能有多個虛函數)。
13.3 虛函數作用
當一個子類繼承了一個含有虛函數的基類,并重寫了該基類中的一個虛函數,我們就說這兩個類構成多態。子類繼承基類的同時,基類的虛函數表也被子類繼承,不同的是被 子類重寫的虛函數將會替代原來虛函數表中對應的基類的虛函數的地址 。從而基類與子類調用同名的虛函數時,所調用的就不是同一個函數,從而體現了多態和虛函數表的作用。
13.4 靜態函數與虛函數的區別
我們知道類的靜態函數是沒有this指針的,調用它時不需要創建對象,通過:類名 ::函數名(參數)的形式直接調用。靜態函數只有唯一的一份,因此它的 地址是固定不變的 , 所以編譯的時候但凡遇到調用該靜態函數的時候就知道調用的是哪一個函數,因此說靜態函數在編譯的時候就已經確定運行時機。 而虛函數則不然,看下面的代碼:
class A
{
public:
virtual void fun()
{
cout<<"i am A <I am B" <fun();
return 0;
}
類A與類B構成多態,創建了 A類指針pb指向 B類對象,當程序編譯的時候只對語法等進行檢測,該語句沒有什么問題,但是編譯器此時無法確定調用的是哪一個 fun() 函數,因為類A類B中都含有fun函數,因此只能是在程序運行的時候通過 pb指針查看對象的虛函數表(訪問虛函數表就是所謂的訪問內存)才能確定該函數的地址,即確定調用的是哪一個函數。這就解釋了所說的“ 虛函數在運行的時候動態綁定。虛函數因為用了虛函數表機制,調用的時候會增加一次內存開銷。 ”
14 重載和覆蓋
14.1 重載
兩個函數名相同,但是參數列表不同(個數,類型),返回值類型沒有要求,在同一作用域中。
14.2 重寫
子類繼承了父類,父類中的函數是虛函數,在子類中重新定義了這個虛函數,這種情況是重寫,是一種同名覆蓋。
15 在main函數前先運行的函數
1.test0 :__attribute((constructor))
是gcc擴展,標記這個函數應當在main函數之前執行。同樣有一個__attribute((destructor))
,標記函數應當在程序結束之前(main結束之后,或者調用了exit后)執行。
2.test1 :全局static變量的初始化在程序初始階段,先于main函數的執行。
#include
using namespace std;
__attribute((constructor)) void test0()
{
printf("before main 0\\n");
}
int test1()
{
cout << "before main 1" << endl;
return 54;
}
static int i = test1();
int main(int argc, char** argv)
{
cout << "main function." <<endl;
return 0;
}
在leetcode里經常見到static,在main之前關閉cin與stdin的同步來“加快”速度的黑科技。
static int _ = []{
cin.sync_with_stdio(false);
return 0;
}();
16 內存管理
在 C++ 中,虛擬內存分為代碼段、數據段、BSS 段、堆區、文件映射區以及棧區六部分。
- 棧(stack) :程序 自動分配 ,使用棧空間存儲函數的返回地址、參數、局部變量、返回值。
- 堆(heap) :
- 堆 :調用
malloc
在堆區動態分配內存,調用free
來手動釋放。堆是操作系統所維護的一塊特殊內存,它提供了動態分配的功能。 - 自由存儲區 :由
new
分配內存,用來delete
手動釋放。和堆類似,通過new
來申請的內存區域可稱為自由存儲區。
- 靜態/全局區 :在 C++ 里面沒有區分bss和data。
- bss段 :存儲未初始化的全局變量和靜態變量(局部+全局),以及所有被初始化為0的全局變量和靜態變量,Block Started by Symbol。
- data段 :存儲程序中已初始化的全局變量和靜態變量。
- 代碼區 (code segment 或 text segment):
- 代碼段 :存放函數體的二進制代碼,text段。
- 常量區 :只讀數據,比如字符串常量,程序結束時由系統釋放。 rodata段 ,read only。
init段:程序初始化入口代碼,在main() 之前運行。
17 常量const
常量是固定值,在程序執行期間不會改變。常量可以是任何的基本數據類型,可分為int、float、char、string和bool。常量定義必須初始化。
17.1 存儲區域
- 局部常量 ,存放在 棧區 ;
- static/全局常量 ,存放在 靜態/全局存儲區 ;
- 字面值常量 ,其值一望而知,存放在 常量區 。
17.2 const修飾成員函數
const 修飾的成員函數表明函數調用 不會對對象做出任何更改 ,事實上,如果確認不會對對象做更改,就應該為函數加上 const 限定,這樣無論 const 對象還是普通對象都可以調用該函數
若同時定義了兩個函數,一個帶 const,一個不帶,這相當于函數的 重載 。
18 代碼解析
18.1 strcpy和strlen
strcpy 是字符串拷貝函數,原型:
char *strcpy(char* dest, const char *src);
從 src 逐字節拷貝到 dest,直到遇到'\\0'結束,因為沒有指定長度,可能會導致拷貝越界,造成緩沖區溢出漏洞,安全版本是 strncpy 函數。
strlen 函數是計算字符串長度的函數,返回從開始到'\\0'之間的字符個數。
18.2 ++i和i++
++i 實現:
int& int::operator++()
{
*this +=1;
return *this;
}
i++ 實現:
const int int::operator(int)
{
int oldValue = *this;
++(*this);
return oldValue;
}
18.3 代碼的區別
(1)字符串 123 保存在 常量區 ,const 本來是修飾 arr 指向的值,不能通過 arr 去修改,但是字符串“123”在常量區,本來就不能改變,所以加不加 const 效果都一樣:
const char * arr = "123";
(2)字符串 123 保存在常量區,這個 和arr 指針指向的是同一個位置,同樣不能通過 brr 去修改"123"的值:
char * brr = "123";
(3)這里 123 本來是在棧上的,但是編譯器可能會做某些優化,將其放到常量區:
const char crr[] = "123";
(4)字符串 123 保存在 棧區 ,可以通過 drr 去修改:
char drr[] = "123";
19 編程題
19.1 點是在三角形內
給定三角形ABC和一點P(x,y,z),判斷點P是否在ABC內。
根據面積法,如果P在三角形ABC內,那么三角形ABP的面積+三角形BCP的面積+三角形ACP的面積應該等于三角形ABC的面積。
S=(x_1y_2+x_2y_3+x_3y_1-x_1y_3-x_2y_1-x_3y_2)/2
代碼如下:
#include
#include
using namespace std;
#define ABS_FLOAT_0 0.0001
struct point_float
{
float x;
float y;
};
float GetTriangleSquar(const point_float pt0, const point_float pt1, const point_float pt2) // 計算三角形面積
{
point_float AB, BC;
AB.x = pt1.x - pt0.x;
AB.y = pt1.y - pt0.y;
BC.x = pt2.x - pt1.x;
BC.y = pt2.y - pt1.y;
return fabs((AB.x * BC.y - AB.y * BC.x)) / 2.0f;
}
bool IsInTriangle(const point_float A, const point_float B, const point_float C, const point_float D) // 判斷給定一點是否在三角形內或邊上
{
float SABC, SADB, SBDC, SADC;
SABC = GetTriangleSquar(A, B, C);
SADB = GetTriangleSquar(A, D, B);
SBDC = GetTriangleSquar(B, D, C);
SADC = GetTriangleSquar(A, D, C);
float SumSuqar = SADB + SBDC + SADC;
if ((-ABS_FLOAT_0 < (SABC - SumSuqar)) && ((SABC - SumSuqar) < ABS_FLOAT_0))
return true;
else
return false;
}
19.2 判斷一個數是二的倍數
判斷一個數是不是二的倍數,即判斷該數二進制末位是不是 0:
a % 2 == 0
a & 0x0001 == 0 // 兩種辦法都可
19.3 一個數中有幾個1
可以直接逐位除十取余判斷:
int fun(long x)
{
int _count = 0;
while(x)
{
if(x % 10 == 1)
++_count;
x /= 10;
}
return _count;
}
int main()
{
cout << fun(123321) << endl;
return 0;
}
審核編輯:湯梓紅
-
C++
+關注
關注
22文章
2108瀏覽量
73651 -
static
+關注
關注
0文章
33瀏覽量
10371 -
關鍵字
+關注
關注
0文章
37瀏覽量
6896
發布評論請先 登錄
相關推薦
評論