目錄
- 介紹
- 背景
- 什么是指標?
- 什么是指向指標的指標?
- 懸空指標
- 指標從哪里開始?
- 區域變數和指標之間的差異
- 那么什么時候沒有指標就不可能實作?
- 但是,有那么多神奇的智能指標!
- 什么是呼叫堆疊和堆疊溢位..
- 不能沒有指標-案例1
- 不能沒有指標-案例2
- 不能沒有指標-案例3
- 不能沒有指標-案例4
- 不能沒有指標-案例5
- 不能沒有指標-案例6
- 為什么指標如此強大
- 物件指標和功能指標之間的區別
- 按值傳遞引數
- 通過參考傳遞引數
- 興趣點
介紹
指標是很多初學者的生死大敵,堪稱c++的嘉峪關,在本文中,我將嘗試闡明有關指標及其用法的某些要點, 希望對正在學習的你有所幫助,
背景
有時,在Web上,我會進行無數次有關C或C ++等語言的指標的討論,關于使用指標是否值得甚至是明智的爭論不斷, 這個問題似乎一直持續到今天, 因此,希望澄清一些事情,我決定創建這篇文章, 我不會對這個問題過于熟練,也不會虔誠地要求每個人 new必須具有相應的 delete或每個人 malloc必須具有相應的 free,
本文適用于C和C ++,您除了處理原始指標外別無選擇, 其他語言(例如Java,C#等)將所有的伏都教魔術隱藏在幕后, 它也適合那些不使用ASM,C或C ++撰寫代碼但被主題迷惑的初學者,
我將嘗試涵蓋所有情況,而不會太深入技術細節, 如果您希望對指標有非常深入的技術知識,那么從Wikipedia到眾多博客都可以找到很多資訊, cplusplus dot com網站上有一篇很棒的關于指標的文章,因此在這里我不再重復, 鑒于您正在使用C或C ++進行開發,因此我將在以下場景中進行介紹:
- 什么時候不使用指標就不可能實作某些東西
- 使用指標的優點
- 通過值,參考或指標傳遞變數
- 懸空指標
什么是指標?
指標是一個整數變數保持在所述特定的寬度(一個地址 float, double, int, struct, class,等等)值被存盤在計算機存盤器中, 因此,指標是計算機在金屬層上本機可以理解的“事實”物件, 指標始終是寬度為8、16、32、64、128等位的無符號整數, 這在很大程度上取決于CPU的主暫存器寬度, 它也由作業系統運行時位對齊指示, 完全有可能在64位CPU上運行16位OS,但反之亦然, 但是,在16位OS中,即使CPU為64位寬,您也將被限制在16位地址空間中,
旁記:由于64位暫存器的寬度足以滿足上述16位OS的需要,因此可以撰寫一個特殊的分段記憶體管理器,該管理器可以窺視超過64Kb的限制,但前提是物理暫存器必須足夠寬(哦,舊的16位Windows) ), 16位記憶體段和偏移量的主題在今天已經過時了, 在當前的討論中,我僅針對平面記憶體,
指標的寬度與該指標可以容納的可尋址記憶體直接相關,并且其寬度的乘冪為2減1,
通過查看此表,您可以看到過去和將來,
如果您要在sifi小說領域中醒來,首先要檢查的是它們使用的計算機上的可尋址指標存盤器,以了解您要處理的內容 ,

當今的64位處理器中確實存在128位暫存器,通常用于SIMD(單指令多資料)操作碼, 長話短說–它們允許并排加載四個32位整數到同一個暫存器中,并在一個CPU周期(赫茲)中執行多個指令,例如MulDiv等, 值得一看:1 EB等于一百萬TB,
好, 那么什么是指向指標的指標?
指向指標的指標是一個整數變數,它保存指標的地址, 它通常用作函式的回傳值, 它也用于可能出現懸掛指標的危險的地方,
//
// Calling a function that returns a pointer
//
void* ptr = nullptr;
// declared as void SomeFuncReturnsPtr(void** p) { *p = value; }
SomeFuncReturnsPtr(&ptr);
// ptr is no longer null
ptr->DoStuff();
懸空指標
作為直接指標(又稱為指標副本)傳遞給不同函式的指標面臨著以下危險:如果該函式洗掉了該指標,則該指標在另一個函式中的另一個副本不會無效, 它仍然保留已擦除記憶體的地址,因此成為懸空指標,因為它的狀態似乎是有效的(不是 null), 此類指標結果的任何用法都是未定義的運行時行為, 在非托管環境中,這是一個真正的危險,因為它在運行時很難找到,因此最好在編碼階段需要面對,
以下代碼防御性地處理了指標懸空的可能性:
// Disastrous main
void main()
{
A* ptr = new A;
NukeA(ptr);
// ptr now is a dangling pointer
assert(ptr == nullptr); // Kaboom. Still points to wiped memory
delete ptr; // Kaboom
}
// Safe main
void main()
{
A* ptr = new A;
NukeSafelyA (&ptr);
// ptr now is null
assert(ptr == nullptr);
}
void NukeA(A* p)
{
p->DoThis();
p->DoThat();
delete p; // Kaboom! A dangling pointer is born
p = nullptr;
}
void NukeSafelyA(A** p)
{
(*p)->DoThis();
(*p)->DoThat();
delete *p; // Nice
*p = nullptr;
}
實際上,如果您使用指標來動態分配記憶體,則永遠不要將指標傳遞給另一個函式,尤其是該函式可以或可能洗掉它時, 僅通過指標將其傳遞給指標,然后 了該指標并 中將 則您的指標將變為 null如果實際上洗掉 在另一個函式 其無效, , 這是一個非常簡單的示例,但確實演示了何時可以發生,
也許干巴巴的文字看起來有寫枯燥,如果單看文字不是很容易消化的話,可以進群973961276來跟大家一起交流學習,群里也有許多視頻資料和技術大牛,配合文章一起理解應該會讓你有不錯的識訓,
推薦一個不錯的c/c++ 初學者課程,這個跟以往所見到的只會空談理論的有所不同,這個課程是從六個可以寫在簡歷上的企業級專案入手帶領大家學習c/c++,正在學習的朋友可以了解一下,
指標從哪里開始?
計算機金屬根本不在乎甚至不知道變數是什么或變數是什么, 它所了解的只是CPU暫存器和記憶體地址-指標, 話雖這么說,編譯器通過變數宣告創建了一種錯覺,使CPU可以將其與內部暫存器以及從中加載該值的記憶體地址相關聯,
區域變數和指標之間的差異
所有區域變數都被宣告并駐留在函式堆疊框架中, 指向動態分配記憶體的指標也位于堆疊幀上,但它指向程式的全域堆記憶體, 在不受管理的全域堆中,程式有責任對其進行管理, 因此,任何泄漏的記憶體最終都會導致資源匱乏,并使您的程式遲早崩潰,
那么什么時候沒有指標就不可能實作?
我可以列舉幾種這樣的情況,但是讓我在進一步嘗試之前先解決一下, 每個程式都有多個功能堆疊框架和一個全域堆, 全域堆基本上是OS管理的虛擬記憶體, 堆疊是LIFO(后進先出)受限大小的資料結構, 可以想象,到相鄰堆疊的任何溢位都將有效擦除已保存的資料并使程式崩潰,
但是,有那么多神奇的智能指標!
記住這一點, 沒有任何代碼看起來和行為像由不知道指標實際是什么或代表什么的人撰寫的智能指標所騎的代碼一樣糟糕, 在嘗試使用智能指標沉迷程式之前,請閱讀以下所有情況, 另外,回傳并重新閱讀“懸空指標”部分, 當通過資產而不是通過腦細胞處理原始指標到智能指標的連接/分離時,智能指標會創建此類底層而臭名昭著,
什么是呼叫堆疊和堆疊溢位
堆疊是LIFO資料結構, 實際上,編譯程式也稱為“堆疊計算機”, 每個行程和執行緒都有自己的堆疊, 每個堆疊都細分為呼叫堆疊, 呼叫堆疊的數量與程式中函式的數量完全匹配, 呼叫堆疊是堆疊中的較小塊, 呼叫堆疊大小有一個限制,通常為1Mb, 在UNIX系統上,它是一個環境變數(我相信), Visual C ++編譯器允許您使用 來更改呼叫堆疊的大小 /F標志 ,
堆疊溢位最好通過以下 偽代碼來表征 _chkstk()函式 :
;***
;_chkstk - check stack upon procedure entry
;
;Purpose:
; Provide stack checking on procedure entry. Method is to simply probe
; each page of memory required for the stack in descending order. This
; causes the necessary pages of memory to be allocated via the guard
; page scheme, if possible. In the event of failure, the OS raises the
; _XCPT_UNABLE_TO_GROW_STACK exception.
;
; NOTE: Currently, the (EAX < _PAGESIZE_) code path falls through
; to the "lastpage" label of the (EAX >= _PAGESIZE_) code path. This
; is small; a minor speed optimization would be to special case
; this up top. This would avoid the painful save/restore of
; ecx and would shorten the code path by 4-6 instructions.
;
;Entry:
; EAX = size of local frame
;
;Exit:
; ESP = new stackframe, if successful
;
;Uses:
; EAX
;
;Exceptions:
; _XCPT_GUARD_PAGE_VIOLATION - May be raised on a page probe. NEVER TRAP
; THIS!!!! It is used by the OS to grow the
; stack on demand.
; _XCPT_UNABLE_TO_GROW_STACK - The stack cannot be grown. More precisely,
; the attempt by the OS memory manager to
; allocate another guard page in response
; to a _XCPT_GUARD_PAGE_VIOLATION has
; failed.
;
;*******************************************************************************
由于堆疊大小有限,因此會監視溢位到相鄰堆疊上的內容,如果發生這種情況,則會引發例外, A _PAGESIZE_在32位作業系統上為4Kb,在64位作業系統上為8Kb,因此,如果任何變數大小大于頁面大小,則會對其進行檢查,但不一定會導致堆疊溢位,
Alloca()函式在運行時從本地堆疊中拉出記憶體,
不能沒有指標-案例1
物件大小超過了函式堆疊的大小, 此限制要求使用全域堆,因此,如果物件太大或隨著時間增長可能會變大,則需要使用指標, 每個人至少遇到一次堆疊溢位例外或分段錯誤,
會是什么 僅舉幾例:
- 讀取在堆疊上宣告的> 1Mb檔案(或大于默認堆疊大小)的字符陣列:
//
// Kaboom – Stack overflow
//
char file_readin[2000000]; //to read a file that of that size or less
...
- 嵌套類和陣列的C ++物件,其累積大小大于1Mb(或大于默認堆疊大小):
class CGiganticClass{…}; // sizeof(CGiganticClass) >= 1,048,576 bytes
//
// Kaboom – Stack overflow
//
CGiganticClass a;
- 遞回函式可呼叫數千個呼叫, 在這種情況下,即使指標也無法挽救您,但可能會延遲不可避免的情況:
//
// Kaboom – Stack overflow
//
void recursive_function(int value)
{
char file_readin[200];
for(int i = 0; i < 100000; i++)
{
recursive_function(i);
}
}
- 集合的實作:
//
// Futile attempt to write a collection that uses stack only
//
template class futile_array<class T>
{
T arr[1000]; // zeroed in ctor
size_t avail_index; // zeroed in ctor
public:
void add(const T val)
{
if(avail_index >= 1000)
return;
arr[avail_index++] = val;
}
};
void main()
{
//
// Kaboom – Stack overflow
//
futile_array<CGiganticClass> a;
}
- 使用的子系統在編譯期間不可用,僅在運行時存在,因此僅回傳指標(Windows API,DirectX API,Linux sys呼叫,Open GL等):
void main()
{
//
// Available only during runtime, but not compile time
//
void* ptr = ::SomeOperatingSysAPI();
// cast and do whatever you need
SomeStruct* p = (SomeStruct*)ptr;
}
- 任何其他型別的運行時創建或銷毀,
如果您使用的是嵌入式系統,情況將會更加糟糕, 在那里,您將很幸運獲得4Kb甚至更小的堆疊大小,
不能沒有指標-案例2
受控創建, 通過宣告指標,不會創建任何物件, 并且即使它們指向較大的物件,相應的指標在記憶體中也僅占據4位元組(32位指標)或8位元組(64位指標), 您可能只想在運行時中且在某些條件下(例如在任何其他情況下)絕對必要時才創建該物件, 因此,控制程式的記憶體消耗,
不能沒有指標-案例3
與案例2相反,控制破壞, 自動變數在函式退出后以相反的順序銷毀, 這也適用于主要功能, 有時,有必要在函式結束之前進行清理, 您可以釋放一些資源,卸載動態庫,等等, 想象一個類,例如,包含指向DLL物件的指標,并且該類被宣告為堆疊變數, 因此,您可以在函式回傳和類解構式呼叫之前卸載該DLL, 并且,如果您的類解構式恰好在同一DLL上執行清理任務,則您的程式將崩潰, 在回傳任何函式之前和釋放該DLL之前,必須銷毀此類物件, 因此,您需要控制該物件破壞的時間,
我知道您會很聰明,并且您會爭辯說,使用函式本身內的嵌套括號來控制自動變數的銷毀是可以實作的, 是的,但前提是不需要在這些花括號之外使用該變數,而且看起來很丑,
不管潛在的情況是什么,要記住的最重要的事項是您可以及時地或在您選擇的特定情況下物理控制物件的死亡,
旁白:BTW智能指標無法提供“受控破壞”功能, 他們從字面上將您的指標砍入一個“堆疊變數”,每個副本都會對其進行參考計數,并在整個位置進行復制, 僅當智能指標提供了洗掉基礎指標的呼叫時,才可以控制指標的生存期,但是它比對delete運算子本身的呼叫更為丑陋和隱秘, 反正我的意見,
同樣,弱指標是強智能指標的觀察者,并且強智能指標實際上在內部攜帶每個弱指標副本的陣列,以便可以將基本強指標消亡通知給它們, 每個弱指標實體都會在一個強指標內添加到陣列,并且實際上可以在內部達到一個相當大的大小,與常規的原始指標相比,這在性能上要差得多,
我不主張這樣做,但是您必須知道成本是什么,是否值得,
不能沒有指標-案例4
單個大小太大的C或C ++物件的集合,以至于不必要地對其進行任何復制或移動都會嚴重破壞堆,以至于對運算子new(或malloc)的任何連續呼叫最終都將導致記憶體不足例外, 這可能是什么? 資料庫引擎實作,一種在場景中擁有50,??000個物件的游戲,其中包含網格和紋理以及其他資料,僅舉幾例, 在這種情況下,在分配了這些物件之后,應避免這些物件的任何復制或移動, 它們必須保留在最初創建的位置,并且對它們的任何操作(例如函式呼叫值,排序等)必須僅通過指標完成,而不能通過物件本身完成, 例如,您可以基于某些物件條件對物件指標進行排序,而無需在記憶體本身中重新排列物件, 此類物件由指標存盤在集合中, 因此, std::vector<CGiganticClass*>存盤為指標,并按指標而不是實際的類進行排序, 這可能是一個令人困惑的情況,可以通過一個示例來解決, 任何工業強度集合(例如 std::vector內部)都使用堆來創建型別, 但是,它在堆上恰好為“型別”,物件本身或指向物件的指標分配了空間, 在宣告指向指標的向量的情況下,將永遠不會創建任何物件,而只會創建指向它們的指標, 因此,您實際上可以稍后分配它們, 或者,如果已經創建/分配了它們,則將指標添加到集合將不會導致物件復制或移動本身, 只能通過僅4或8個位元組寬的指標進行復制和移動,
不能沒有指標-案例5
如上所述,在情況4中,通過指標存盤物件提供了另一個絕佳的優勢, 您不僅可以存盤在指標陣列,但所有在同一時間,你可以在這些非常相同的指標存盤 std::map, std::unordered_map, std::list, std::hashmap同時,從來沒有產生不必要的物件副本, 您可能希望通過索引,鍵值或任何其他有效的搜索模式對其進行訪問,并獲得指向要查找內容的指標,而主物件在堆記憶體中保持靜態, 這甚至使資料繁重的程式也變得例外快速和回應,
不能沒有指標-案例6
指標傳遞的異步執行引數或更好地稱為執行緒引數,它們實際上是呼叫函式內的堆疊自動變數, 好吧,任何試圖通過地址將堆疊變數傳遞到獨立執行緒中的嘗試都是不可避免的災難的關鍵, 因為當呼叫函式退出時,變數將被破壞,并且相應的執行緒指標將在查找并訪問無人區, 這也適用于錯誤使用智能指標,該智能指標在函式退出后洗掉了物件,并使執行緒指標處于懸空狀態,
為什么指標如此強大
指標是有狀態的物件, 它不僅可以訪問物件本身,而且還可以同時保存有關物件是否存在的資訊,
物件指標和功能指標之間的區別
宣告約定, 物件指標指向資料段,而函式指標指向代碼段, 另外,不需要動態分配或洗掉函式指標,
按值傳遞引數
你說什么? 每個人都知道! 為什么還要費心講這個話題, 好吧,好吧,檢查一下堆疊部分的圖–引數, 在跳轉到函式的標簽之前,每個引數都會被壓入該堆疊,根據該物件的組成,它可能是很多PSH匯編操作碼, 還有一個為什么陣列始終由指標傳遞的另一個原因,即使它們沒有明確指定, 順便說一句,這僅適用于C陣列,如果您傳遞類似的東西 std::vector,它將按值傳遞或復制, 如果您需要計算函式跳轉需要多少個CPU周期,則取決于物件的大小, 因為它是完整且獨立的副本,所以該推送也可能導致堆疊溢位, 有時,您別無選擇,只能傳遞價值, 但是,當您不必這樣做時,通過參考或指標進行傳遞的速度要快數百倍,因為它只是一個4或8位元組的推送,而不是數百次的推送,
通過參考傳遞引數
它使您可以通過一個PSH操作碼將一個巨大的物件傳遞到一個函式中,并可以對其進行訪問,就好像它不是指標一樣, 但這要付出另外的代價, 首先,您可能會意外傳入一個已取消參考的空指標,并且您的函式將崩潰, 其次,由于參考本質上是無狀態的,因此絕對沒有辦法檢查參考是否良好, 這提出了一個有趣的觀點,即如果物件是指向動態分配的記憶體的指標,則不要通過參考(取消參考)將其傳遞,而應通過指標將其傳遞,
興趣點
值得一提的是,即使您的計算機僅安裝了1 GB的物理記憶體,一個32位作業系統也可以在每個行程中尋址整個4 GB的地址空間, 這種魔術是通過磁盤檔案系統執行的,因此將不適合物理RAM的內容寫入磁盤或“分頁”(使其在某種程度上取決于磁盤的讀/寫速度), 實際上,每個行程都有自己的單獨4 GB地址空間(虛擬),而不管您的硬體RAM容量如何(達到一定程度), 不僅如此,每個行程還擁有自己的虛擬處理器, 作業系統在切換行程/執行緒背景關系本身時會切換處理器值, 對于處理器暫存器而言,保持指向當前行程記憶體而不是相鄰行程記憶體的指標非常重要, 一個行程中的任何崩潰都不會損害任何其他行程, 在舊的16位系統(如MSDOS或Windows 1.0、2.0、3.0和3.1)中,情況并非如此, 這些在物理硬體金屬本身和一個崩潰的程式上運行的作業系統可能實際上使其他任何程式和OS崩潰,
讓你不再害怕C語言中的指標(上)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/247748.html
標籤:其他
