底軟驅動 | C++內存相關

文章目錄

  • C++內存相關
    • C++內存分區
      • C++對象的成員函數存放在內存哪里
    • 堆和棧的區別
    • 堆和棧的訪問效率
    • “野指針”
    • 有了malloc/free為什么還要new/delete
    • alloca
    • 內存崩潰
    • C++內存泄漏的幾種情況
    • 內存對齊
    • 柔性數組
    • 參考
    • 推薦閱讀

C++內存相關

本篇介紹了 C++ 內存相關的知識。

C++內存分區

在C++中,內存分成5個區,他們分別是堆、棧、自由存儲區、全局/靜態存儲區和常量存儲區。

  • :在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置于處理器的指令集中,效率很高,但是分配的內存容量有限。
  • :就是那些由 new分配的內存塊,他們的釋放編譯器不去管,由我們的應用程序去控制,一般一個new就要對應一個 delete。如果程序員沒有釋放掉,那么在程序結束后,操作系統會自動回收。
  • 全局/靜態存儲區:全局變量和靜態變量被分配到同一塊內存中。在以前的C語言中,全局變量又分為初始化的和未初始化的。在C++里面沒有這個區分了,他們共同占用同一塊內存區。
  • 常量存儲區:這是一塊比較特殊的存儲區,他們里面存放的是常量,不允許修改。
  • 代碼段:代碼段(code segment / text segment)通常是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定,并且內存區域通常屬于只讀, 某些架構也允許代碼段為可寫,即允許修改程序。在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。

根據c/c++對象生命周期不同,c/c++的內存模型有三種不同的內存區域,即

  • 自由存儲區,動態區、靜態區。

  • 自由存儲區:局部非靜態變量的存儲區域,即平常所說的棧。

  • 動態區: 用operator new ,malloc分配的內存,即平常所說的堆。

  • 靜態區:全局變量 靜態變量 字符串常量存在位置。

下圖為 C++ 內存模型,來自C++ Essentials。

  • .text 部分是編譯后程序的主體,也就是程序的機器指令。
  • .data 和 .bss 保存了程序的全局變量,.data保存有初始化的全局變量,.bss保存只有聲明沒有初始化的全局變量。
  • heap(堆)中保存程序中動態分配的內存,比如 C 的malloc申請的內存,或者C++中new申請的內存。堆向高地址方向增長。
  • stack(棧)用來進行函數調用,保存函數參數,臨時變量,返回地址等。
  • 共享內存的位置在堆和棧之間。

更詳細的內存段解釋見C與C++內存管理詳解。

下面的文章介紹了Linux虛擬地址空間布局。

  • x86 程序內存堆棧模型
  • Linux 中的各種棧:進程棧 線程棧 內核棧 中斷棧

C++對象的成員函數存放在內存哪里

類成員函數和非成員函數代碼存放在代碼段。如果類有虛函數,則該類就會存在虛函數表。虛函數表在Linux/Unix 中存放在可執行文件的只讀數據段中(rodata),即前面起到的代碼段,而微軟的編譯器將虛函數表存放在常量段

堆和棧的區別

管理方式:對于棧來講,是由編譯器自動管理,無需我們手工控制;對于堆來說,釋放工作由程序員控制,容易產生memory leak

空間大小:一般來講在 32 位系統下,堆內存可以達到 4G 的空間,從這個角度來看堆內存幾乎是沒有什么限制的。但是對于棧來講,棧頂和棧底是之前預設好的,大小固定,可以通過ulimit -a查看,使用ulimit -s修改。

碎片問題:對于堆來講,頻繁的new/delete勢必會造成內存空間的不連續,從而造成大量的碎片,使程序效率降低。對于棧來講,則不會存在這個問題,因為棧是先進后出的隊列,它們是如此的一一對應,以至于永遠都不可能有一個內存塊從棧中間彈出,在他彈出之前,在他上面的后進的棧內容已經被彈出。

生長方向:對于堆來講,生長方向是向上的,也就是向著內存地址增加的方向;對于棧來講,它的生長方向是向下的,是向著內存地址減小的方向增長。

分配方式:堆都是動態分配的,沒有靜態分配的堆。棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變量的分配。動態分配由alloca函數進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。

分配效率:棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是C/C++函數庫提供的,它的機制是很復雜的,例如為了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由于內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,這樣就有機會分到足夠大小的內存,然后進行返回。顯然,堆的效率比棧要低得多。

從這里我們可以看到,堆和棧相比,由于大量new/delete的使用,容易造成大量的內存碎片;由于沒有專門的系統支持,效率很低;由于可能引發用戶態和核心態的切換,內存的申請,代價變得更加昂貴。所以棧在程序中是應用最廣泛的,就算是函數的調用也利用棧去完成,函數調用過程中的參數,返回地址,EBP和局部變量都采用棧的方式存放。所以,我們推薦大家盡量用棧,而不是用堆。

雖然棧有如此眾多的好處,但是由于和堆相比不是那么靈活,有時候分配大量的內存空間,還是用堆好一些。

堆和棧的訪問效率

  • 堆和棧訪問效率哪個更高
  • 棧為什么效率比堆高

“野指針”

“野指針”不是NULL指針,是指向“垃圾”內存的指針。“野指針”的成因主要有三種:

  1. 指針變量沒有被初始化,缺省值是隨機的;
  2. 指針被free/delete之后,沒有置為NULL,讓人誤以為該指針是個合法的指針;
  3. 指針操作超越了變量的作用域范圍(內存越界)。

有了malloc/free為什么還要new/delete

mallocfree是C++/C語言的標準庫函數,new/delete是C++的運算符。它們都可用于申請動態內存和釋放內存。

對于非內部數據類型的對象而言,光用maloc/free無法滿足動態對象的要求。**對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。**由于malloc/free是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加于malloc/free。因此 C++ 語言需要一個能完成動態內存分配和初始化工作的運算符new,以及一個能完成清理與釋放內存工作的運算符delete

既然new/delete的功能完全覆蓋了malloc/free,為什么C++不把malloc/free淘汰出局呢?這是因為C++程序經常要調用C函數,而C程序只能用malloc/free管理動態內存

如果用free釋放“new創建的動態對象”,那么該對象因無法執行析構函數而可能導致程序出錯。如果用delete釋放“malloc申請的動態內存”,結果也會導致程序出錯,該程序的可讀性也很差。所以new/delete必須配對使用,malloc/free也一樣。

alloca

man中的介紹:

The alloca() function allocates size bytes of space in the stack frame of the caller. This temporary space is automatically freed when the function that called alloca() returns to its caller.

alloca是從棧中分配空間。正因其從棧中分配的內存,因此無需手動釋放內存。

討論見stackoverflow。

內存崩潰

錯誤類型原因備注
聲明錯誤變量未聲明編譯時錯誤
初始化錯誤未初始化或初始化錯誤運行不正確
訪問錯誤1. 數組索引訪問越界
2. 指針對象訪問越界
3. 訪問空指針對象
4. 訪問無效指針對象
5. 迭代器訪問越界
內存泄漏1. 內存未釋放
2. 內存局部釋放
參數錯誤本地代理、空指針、強制轉換
堆棧溢出1. 遞歸調用
2. 循環調用
3. 消息循環
4.大對象參數
5. 大對象變量
參數、局部變量都在棧(Stack)上分配
轉換錯誤有符號類型和無符號類型轉換
內存碎片小內存塊重復分配釋放導致的內存碎片,最后出現內存不足數據對齊,機器字整數倍分配

其它如內存分配失敗創建對象失敗等都是容易理解和相對少見的錯誤,因為目前的系統大部分情況下內存夠用;此外除 0 錯誤也是容易理解和防范。

C++內存泄漏的幾種情況

1. 在類的構造函數和析構函數中沒有匹配的調用new和delete函數

兩種情況下會出現這種內存泄露:一是在堆里創建了對象占用了內存,但是沒有顯示地釋放對象占用的內存;二是在類的構造函數中動態的分配了內存,但是在析構函數中沒有釋放內存或者沒有正確的釋放內存

2. 沒有正確地清除嵌套的對象指針

3. 在釋放對象數組時在delete中沒有使用方括號

方括號是告訴編譯器這個指針指向的是一個對象數組,同時也告訴編譯器正確的對象地址值病調用對象的析構函數,如果沒有方括號,那么這個指針就被默認為只指向一個對象,對象數組中的其他對象的析構函數就不會被調用,結果造成了內存泄露。如果在方括號中間放了一個比對象數組大小還大的數字,那么編譯器就會調用無效對象(內存溢出)的析構函數,會造成堆的奔潰。如果方括號中間的數字值比對象數組的大小小的話,編譯器就不能調用足夠多個析構函數,結果會造成內存泄露。

釋放單個對象、單個基本數據類型的變量或者是基本數據類型的數組不需要大小參數,釋放定義了析構函數的對象數組才需要大小參數。

4. 指向對象的指針數組不等同于對象數組

對象數組是指:數組中存放的是對象,只需要delete []p,即可調用對象數組中的每個對象的析構函數釋放空間

指向對象的指針數組是指:數組中存放的是指向對象的指針,不僅要釋放每個對象的空間,還要釋放每個指針的空間,delete []p只是釋放了每個指針,但是并沒有釋放對象的空間,正確的做法,是通過一個循環,將每個對象釋放了,然后再把指針釋放了

5. 缺少拷貝構造函數

兩次釋放相同的內存是一種錯誤的做法,同時可能會造成堆的崩潰。

按值傳遞會調用(拷貝)構造函數,引用傳遞不會調用。

在C++中,如果沒有定義拷貝構造函數,那么編譯器就會調用默認的拷貝構造函數,會逐個成員拷貝的方式來復制數據成員,如果是以逐個成員拷貝的方式來復制指針被定義為將一個變量的地址賦給另一個變量。這種隱式的指針復制結果就是兩個對象擁有指向同一個動態分配的內存空間的指針。當釋放第一個對象的時候,它的析構函數就會釋放與該對象有關的動態分配的內存空間。而釋放第二個對象的時候,它的析構函數會釋放相同的內存,這樣是錯誤的。

所以,如果一個類里面有指針成員變量,要么必須顯示的寫拷貝構造函數和重載賦值運算符,要么禁用拷貝構造函數和重載賦值運算符。

6. 缺少重載賦值運算符

這種問題跟上述問題類似,也是逐個成員拷貝的方式復制對象,如果這個類的大小是可變的,那么結果就是造成內存泄露,如下圖:

7. 關于nonmodifying運算符重載的常見迷思

a. 返回棧上對象的引用或者指針(也即返回局部對象的引用或者指針)。導致最后返回的是一個空引用或者空指針,因此變成野指針。

b. 返回內部靜態對象的引用。

c. 返回一個泄露內存的動態分配的對象。導致內存泄露,并且無法回收。

解決這一類問題的辦法是重載運算符函數的返回值不是類型的引用,二應該是類型的返回值,即不是 int&而是int

8. 沒有將基類的析構函數定義為虛函數

當基類指針指向子類對象時,如果基類的析構函數不是virtual,那么子類的析構函數將不會被調用,子類的資源沒有正確是釋放,因此造成內存泄露。

9. 野指針:指向被釋放的或者訪問受限內存的指針

造成野指針的原因:

  1. 指針變量沒有被初始化(如果值不定,可以初始化為NULL)。
  2. 指針被free或者delete后,沒有置為NULLfreedelete只是把指針所指向的內存給釋放掉,并沒有把指針本身干掉,此時指針指向的是“垃圾”內存。釋放后的指針應該被置為NULL
  3. 指針操作超越了變量的作用范圍,比如返回指向棧內存的指針就是野指針。

內存對齊

CPU是按字讀取內存。所以內存對齊的話,不會出現某個類型的數據讀一半的情況,需要再二次讀取內存。可以提升訪問效率。

內存對齊的作用:

  • 可移植性:因為不同平臺對數據的在內存中的訪問規則不同,不是所有的硬件都可以訪問任意地址上的數據,某些硬件平臺只能在特定的地址開始訪問數據。所以需要內存對齊。
  • 性能原因:一般使用內存對齊可以提高CPU訪問內存的效率。如32位的intel處理器通過總線訪問內存數據,每個總線周期從偶地址開始訪問32位的內存數據,內存數據以字節為單位存放。如果32為的數據沒有存放在4字節整除的內存地址處,那么處理器需要兩個總線周期對數據進行訪問,顯然效率下降很多;另外合理的利用字節對齊可以有效的節省存儲空間。
  • C/C++語言內存對齊
  • 內存對齊的規則以及作用
  • C語言內存對齊
  • 內存對齊
  • C/C++ 各數據類型占用字節數
  • C/C++ 結構體字節對齊
  • C/C++內存對齊

柔性數組

柔性數組結構成員:C99 中,結構中的最后一個元素允許是未知大小的數組,這就叫做柔性數組成員,但結構中的柔性數組成員前面必須至少一個其他成員。柔性數組成員允許結構中包含一個大小可變的數組。sizeof返回的這種結構大小不包括柔性數組的內存。包含柔性數組成員的結構用malloc函數進行內存的動態分配,并且分配的內存應該大于結構的大小,以適應柔性數組的預期大小。

  • C語言0長度數組(可變數組/柔性數組)詳解
  • C99柔性數組成員介紹(其一)
  • C99柔性數組成員介紹(其二)

參考

  • C++ Essentials
  • C和C++內存模型
  • C與C++內存管理詳解
  • C++ 常見崩潰問題分析
  • C++虛函數表
  • 虛函數表在對象內存中的布局
  • C++內存泄漏的幾種情況
  • 實習面經 --C/C++ 基礎

推薦閱讀

  • C/C++程序內存的各種變量存儲區域和各個區域詳解

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/45547.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/45547.shtml
英文地址,請注明出處:http://en.pswp.cn/web/45547.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

力扣第八題——字符串轉換整數

題目介紹 請你來實現一個 myAtoi(string s) 函數,使其能將字符串轉換成一個 32 位有符號整數。 函數 myAtoi(string s) 的算法如下: 空格:讀入字符串并丟棄無用的前導空格(" ")符號:檢查下一個字…

TCP重傳、滑動窗口、流量控制、擁塞控制機制

目錄 1、TCP重傳機制超時重傳快速重傳 2、滑動窗口3、流量控制4、擁塞控制1、慢啟動2、擁塞避免3、擁塞發生 1、TCP重傳機制 TCP 針對數據包丟失的情況,會用重傳機制解決。 超時重傳 就是在發送數據時,設定一個定時器,當超過指定的時間還沒…

Ctrl+C、Ctrl+V、Ctrl+X 和 Ctrl+Z 的起源

注:機翻,未校對。 The Origins of CtrlC, CtrlV, CtrlX, and CtrlZ Explained We use them dozens of times a day: The CtrlZ, CtrlX, CtrlC, and CtrlV shortcuts that trigger Undo, Cut, Copy, and Paste. But where did they come from, and why do…

文件上傳接口

文章目錄 開發前端接口 開發前端接口 首先這個前端的文件上傳組件使用了,前端組件 首先這個接口不是一般的接口,這個接口可以提取出來,之后那里使用了,就直接放到哪里 所以這是一個萬能文件上傳接口 寫完之后選擇 頭像組件 在圖庫中添加組件 寫前端組件之后,寫了前端的組件…

Bootstrap 5 加載效果

Bootstrap 5 加載效果 Bootstrap 5 是一個流行的前端框架,它提供了豐富的組件和工具,用于快速開發響應式和移動優先的網頁。在本文中,我們將探討 Bootstrap 5 中的加載效果,包括如何實現它們以及它們在網頁設計中的作用。 什么是加載效果? 加載效果是在網頁或應用程序中…

k8s集群創建devops項目一直等待狀態,沒有發現host

問題分析: kubesphere在幫我們自動化創建一些智能自動化的額時候難免會發生一些小錯誤,devops-jenkins是一個部署也會生成一個容器組即pod,容器組的容器服務端口是 targetPort,容器組對外暴露的端口是port,拿devops-c…

[深度學習]基于yolov10+streamlit目標檢測演示系統設計

YOLOv10結合Streamlit構建的目標檢測系統,不僅極大地增強了實時目標識別的能力,還通過其直觀的用戶界面實現了對圖片、視頻乃至攝像頭輸入的無縫支持。該系統利用YOLOv10的高效檢測算法,能夠快速準確地識別圖像中的多個對象,并標注…

Billu_b0x靶機

信息收集 使用arp-scan 生成網絡接口地址來查看ip 輸入命令: arp-scan -l 可以查看到我們的目標ip為192.168.187.153 nmap掃描端口開放 輸入命令: nmap -min-rate 10000 -p- 192.168.187.153 可以看到開放2個端口 nmap掃描端口信息 輸入命令&…

配置PYTHONPATH環境變量

配置PYTHONPATH環境變量 前言Win系統臨時配置永久配置 Linux系統臨時配置永久配置 前言 在運行py腳本時不僅需要import官方庫,經常會import自己編寫的腳本,但此時會出現模塊找不到的如下報錯。解決方法是配置PYTHONPATH,下文介紹Win系統和Li…

禹神:一小時快速上手Electron,前端Electron開發教程,筆記。一篇文章入門Electron

一、Electron是什么 簡單的一句話,就是用htmlcssjsnodejs(Native Api)做兼容多個系統(Windows、Linux、Mac)的軟件。 官網解釋如下(有點像繞口令): Electron是一個使用 JavaScript、HTML 和 CSS 構建桌面…

Resources.Load返回null

Resources.Load返回null 在unity中Resources.Load從Assets下的任意Resources目錄下讀取資源&#xff0c;比如從Assets\Resources下讀取Cube&#xff08;預制體&#xff09;&#xff0c;當然也可以讀取其他資源 代碼為 GameObject prefab Resources.Load<GameObject>(…

微軟Edge瀏覽器深度解析:性能、安全性與特色功能全面評測

一、引言 自Windows 10操作系統推出以來&#xff0c;微軟Edge瀏覽器作為默認的網頁瀏覽器&#xff0c;憑借其現代化的設計和出色的性能表現&#xff0c;逐漸獲得了用戶的認可。本文旨在對Edge瀏覽器進行深入分析&#xff0c;探討其在多個方面的表現。 二、界面與操作體驗 界面…

在 PostgreSQL 里如何處理數據的存儲優化和數據庫備份的效率平衡?

&#x1f345;關注博主&#x1f397;? 帶你暢游技術世界&#xff0c;不錯過每一次成長機會&#xff01;&#x1f4da;領書&#xff1a;PostgreSQL 入門到精通.pdf 文章目錄 在 PostgreSQL 里如何處理數據的存儲優化和數據庫備份的效率平衡&#xff1f;一、數據存儲優化&#x…

HTML表格表單及框架標簽

一.表格標簽 1.<table></table> 創建表格 2.<caption></caption> 表格的標題 3.<tr></tr>Table Row&#xff08;表格行&#xff09; 4.<td></td>Table Data&#xff08;表格數據&#xff09;其中有屬性rowspan"2&quo…

Linux操作系統——數據庫

數據庫 sun solaris gnu 1、分類&#xff1a; 大型 中型 小型 ORACLE MYSQL/MSSQL SQLITE DBII powdb 關系型數據庫 2、名詞&#xff1a; DB 數據庫 select update database DBMS 數據…

Go中的defer看似很簡單,實則一點都不難

Golang 中的 Defer 在Go語言中&#xff0c;defer語句用于將一個函數調用推遲到外圍函數返回之后執行。它常用于確保某些操作在函數結束時一定會執行&#xff0c;例如資源釋放、文件關閉等。 基本語法 defer語句的基本使用方法如下&#xff1a; func main() {defer fmt.Prin…

距離變換 Distance Transformation

以下為該學習地址的學習筆記&#xff1a;Distance transformation in image - Python OpenCV - GeeksforGeeks 其他學習資料&#xff1a;Morphology - Distance Transform 簡介 距離變換是一種用于計算圖像中每個像素與最近的非零像素之間距離的技術。它通常用于圖像分割和物體…

51單片機5(GPIO簡介)

一、序言&#xff1a;不論學習什么單片機&#xff0c;最簡單的外設莫過于I口的高低電平的操作&#xff0c;接下來&#xff0c;我們將給大家介紹一下如何在創建好的工程模板上面&#xff0c;通過控制51單片機的GPIO來使我們的開發板上的LED來點亮。 二、51單片機GPIO介紹&#…

第三節SHELL腳本中的變量與運算(1.1-1.5)

一,腳本中的變量 1,1什么是變量 在編寫程序是,通常會遇到被操作對象不固定的情況我們需要用一串固定的字符來的表示不固定的值,這就是變量存在的根本意義變量的實現原理就是內存存儲單元的一個符合名稱 1,2 變量的命名規則 變量的名稱中只能包含數字,大小寫字母以及下劃線 …

PySide在Qt Designer中使用QTableView 顯示表格數據

在 PySide6 中&#xff0c;可以使用 Qt Model View 架構中的 QTableView 部件來顯示和編輯表格數據。 1、創建ui文件 在Qt Designer中新建QMainWindow&#xff0c;命名為csvShow.ui。QMainWindow上有兩個部件&#xff1a;tableview和btn_exit。 2、使用pyuic工具將ui文件轉換為…