C内存2:内存分区
首先要了解內存分區。了解內存分區首先要弄清楚程序在運行之前:
程序在運行之前,我們要想執行我們所編寫的C程序,那么第一部需要對這個程序進行編譯:
- 預處理:宏定義展開,頭文件展開,條件編譯,這里并不會檢查語法。
- 編譯:檢查語法,將預處理后文件編譯,生成匯編文件。
- 匯編:將匯編文件生成目標文件(二進制文件)
- 鏈接:將目標文件連接成可執行程序。
那么這個可執行程序會分成幾個區域:
--------------------------------程序運行之前的分區:
在沒有運行程序前,也就是說程序沒有加載到內存前,可執行程序內部已經分好3段信息,分別為代碼(test)、數據區(data)、和未初始化數據區(bss)3個部分(有些人直接把data和bss合起來叫做靜態區域全局區)。
在程序運行過程中會把全局變量和靜態變量也單獨的給他分配內存,因為全局變量和靜態變量在整個程序運行期間都會存在,他不會自動銷毀、他也不會釋放,程序結束他才會銷毀,所以說我在運行之前就可以把這個內存給他確定好,確定好之后你就直接用就完事啦。我就不需要在運行的時候給他分配內存啦,我就提前把這塊內存給他分配好,提前把準備工作做好,運行的時候就省事。
---------------------為什么這個時候棧和堆的概念沒有呢?--------------------
我們要說到臨時變量,這個變量在程序的運行過程中才有,運行到這之后需要個變量我就申請個變量,用完之后直接銷毀啦,所以像這些變量啊,堆上的變量啊只有在運行的時候才有。上邊的可執行程序當你去執行他的時候,這個程序就加載到內存里,加載進去之后開始執行,這個時候才會有堆區和棧區(運行的時候才會有棧區和堆區)。
總結:程序在運行之前呢只有兩個區,一個是代碼區一個是全局靜態區。其中代碼區就是指令嘛,data和bss就是全局變量和靜態變量,其中靜態變量分為全局靜態變量和局部靜態變量,咱不管他是全局還是局部,反正只要是靜態變量活著都是全局,為什么分為data和bss呢?就是因為有些變量他是沒有初始化的,他把這個初始化和未初始化的全局變量和靜態變量給分開啦,data里邊是初始化的,bss里邊是未初始化的全局和靜態變量。(其實如果一個靜態的全局變量沒有被初始化,編譯器自動給他初始化為0,)
全局靜態區內的變量在編譯階段已經分配好內存空間并初始化,這塊內存在程序運行期間一直存在,它主要存儲全局變量,靜態變量和常量。
PS:在編譯階段分配的內存并不是真正的在物理意義上給他分配內存,而是分配地址,只是給他分配一個地址而已,并不是真正的給他分配一個實實在在的內存,真正的內存只有到程序運行的時候才會有真正的內存,(所以并不是說你定義完啦內存就有,加載到內存之后呢才有。我們說的編譯時候的內存只是分配地址,)
--------------------------------程序運行之后的分區:
其中堆區存放的是我們程序員自己開辟自己管理的數據(堆區(自己申請但是別忘記釋放,數據量大的胡放在堆上)是這個里邊我們唯一能夠手動控制的,在需要的時候我們自己申請,在不需要的時候我們給他釋放;如果你忘記釋放了就會造成內存泄漏,每次開辟了內存都不釋放,最終造成的就是內存不夠用,所以自己開辟的內存要自己釋放,要不就等程序結束的時候釋放,由曹操作系統自動回收,但是這個時候已經晚了);堆區由程序人員手動申請,手動釋放,若不手動釋放,程序結束后由系統回收,生命周期是整個程序運行期間,使用malloc或者new進行堆的申請。
棧區(自動申請自動釋放,不要去管他)上我們是無法控制的是臨時變量,函數結束之后自動釋放(自動申請自動釋放,不需要管);棧的大小是固定的,堆的大小理論上沒有上限,看你自己的機器有多大,所以一般數據量比較大的話我們一般是放在堆區。(要注意局部變量的生命周期,你用它的時候要保證他沒有dead還活著);棧區由系統進行內存的管理,主要存放函數的參數以及局部變量,在函數完成執行,系統自行釋放棧區內存,不需要用戶管理。
在程序加載之后呢,仍然會有一個全局靜態區)(這個區域的數據會一直活著,一直到程序結束),一般情況下全局靜態區放哪些變量呢?放3種:全局變量(全局變量放在全局區)、靜態變量(靜態變量放在靜態區)、常量(常量放在常量區)。
????
//全局區和靜態區
int a;static int a;上邊語句的相似與區別:(1)生命周期一樣(從開始一直到程序結束)。(2)作用域不一樣(int a;是外部鏈接屬性,其實是extern int a;而static int a;是內部鏈接)。外部鏈接屬性是比如你這個int a;是在a.c中定義那么你在b.c中也可以用它。那么你用ststic int a;不好意思,他只能在當前文件內可見,如果你在a.c中定義,那么不好意思在b.c中就不能用啦。//常量區
神惡魔數據會放在常量區,第一是:字符串常量;就是雙引號"hello worle"里邊的東西一般都是放在常量區。
第二是:const修飾的全局變量;也會放在常量區。
常量區的特點:常量區的數據一旦初始化,不能修改(通過指針間接修改,間接賦值也修改不了)。這段內存區域受到保護,是個只讀的內存。
char *="hello word";//這個會直接down掉,為什么呢?因為這個hello word是放在常量區的,這個常量區就是一旦初始化不能修改,(所以意思就是如果hello world放在常量區的話是不能修改的,如果沒有放在常量區是可以修改的)
這里理論上認為hello word就是不能修改的(因為編譯器不同可能會有差異),這里認為char *p=""hello world;這個就是能修改我也不修改,所以以后再寫的時候可以寫成const char *p=""hello world;//有的編譯器,你只傳進去一個hello world,沒有加const,他就會給你報錯,因為它認為你兩個類型不匹配。
? ?棧區例子:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<stdlib.h>//1、戰區的內存自動申請自動釋放,不需要程序手動管理
int *myFunction()
{//3所以這里說不要返回局部變量的地址int a = 10;//01這個內存在棧上,是4return &a;
}//02大括號結束01處的內存就釋放啦void test01()
{//1我們并不關心值是多少,因為局部變量a的內存已經被回收int *p = myFunction();//這里訪問a的時候他已經死掉了//std::cout <<"*p="<<*p <<std::endl;printf("*p=%d\n", *p);//0問題:這里能不能輸出正確結果?//2雖然這里有可能是10,但是如果你的程序很大,這里就不一定是10啦
}char *getString()
{char str[] = "hello world";//03str這個數組在棧上,這個數組也要被釋放; 而"hello world"是字符串常量,在常量區;return str;
}void test02()
{char *s = NULL;s=getString();printf("s=%s\n");//這里輸出亂碼
}
程序結果:
//程序進入test02的時候,發現里邊有一個局部變量,局部變量放在棧上,s是字符指針,指針都占用4個字節。所以在棧上分配4個字節,s代表的是這塊內存,不是這塊內存的值,s代表這塊內存的值是空。下一句執行到getString(),這個里邊有一個數組,這個數組里邊有一個hello world,編譯器看到hello world,先把hello world放到常量區(有時侯先把hello world放到常量區,然后再把hello world拷貝到你這個棧區數組里邊;這里有爭議可能不一樣,有的直接把hello world做個優化,直接把hello world放到數組里邊啦;這里按照先放到常量區是不會錯的);咱們說這個gerString里是一個局部的數組,所以這個數組是在棧上,再把hello world拷貝一份放過去,之后在return str;而數組名str是個地址,所以他返回的是首地址(圖中就是0x002),返回出來地址給s,這個時候s里邊就不是NULL啦,是0x002,但是返回地址之后,這個函數getString就結束啦,所以這里的內存就被干掉啦,那你這個時候通過0x002訪問一個被釋放的內存,那么這個里邊什么樣的值都有可能。
程序解讀:
拷貝放進去
假如這個時候棧上的地址是0x002和0x003,假定他是不連續的。
return地址給s之后大括號收尾內存被干掉
?堆區舉例:
下面畫圖來解釋一下為什么調用test02()函數輸出的p為NULL,首先畫一個棧區,在棧上上定義一個變量p,占用4個字節,值為NULL就是0,下面調用allocateSpace()函數,他是不是有參數啊,參數要入棧,參數也要分配內存空間,你沒有分配內存空間你怎么把void allocateSpace(char *p)中的p給這個allocateSpace(p);中的p啊?所以void allocateSpace(char *p)中的p要分配內存空間,因此棧中第一個p是test02中的p,下邊的是allocateSpace中的p,剛進來的時候void allocateSpace(char *p)中的p要入棧,把這個p賦給第二個p,所以這個p的值其實也為空;接下來執行p = (char *)malloc(100);我們在堆上開辟100個字節,然后將memset(p, 0, 100);100個字節置為0,然后strcpy(p, "hello world");把hello world拷貝到p里邊,這個hello world在常量區,之后把hello world拷貝到堆上。如下:
常量拷貝到堆上就是說malloc在分配完內存之后把這個0x888返回了,返回給p這個變量
此時函數的拷貝也拷貝完啦,那么這個函數void allocateSpace(char *p){...}就結束啦,結束后,你想一下p這個變量要不要釋放?(我們說函數的形參他也是個局部變量,我們把這個void allocateSpace(char *p)中的p叫做形參,里邊的p = (char *)malloc(100);中的p叫做臨時變量,他們兩個其實都是臨時變量,函數結束之后參數和局部變量都要釋放,)
所以test02的p并沒有改(第一你并沒有讓test02的p指向堆空間,第二內存泄漏了,你把堆內存的地址給丟了,釋放完之后就不能找到堆上的地址了,找不到了,所以這塊內存你就釋放不了了,這就是為什么你輸出p的時候打%s一個NULL的輸出)。那么這塊我一定要讓第一個p就是test02中的char *p = NULL;有所指向怎么辦?需要修改見代碼。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<stdlib.h>
//堆的內存,成員手動申請,手動釋放
int *getspace()
{//3p這個變量是4個字節,在棧上;p這個變量內部存的是一個地址,這個地址所指向的就是在堆上的一塊內存;當getspace這個函數停止后,p這個變量會釋放掉(因為在棧上),那么這個堆上的4*5=20個字節會不會釋放掉?不會,因為我們沒有手動free的話,他是不會釋放掉的,要么得等程序結束。int *p = (int *)malloc(sizeof(int)*5);//2在堆上分配空間,為什么*5防止報錯if (NULL == p){return NULL;}//1只要是連續的內存空間,都能使用下標的方式訪問內存for (int i = 0; i < 5; ++i){p[i] = 100 + i;}return p;
}
void test01()
{//4函數調用返回之后,上邊3處p變量就先把它保存的值拷貝給我外部ret這個變量(p把他的值直接給我ret),ret就占malloc(sizeof(int)*5)這塊內存,3處的p就可以放心的釋放掉啦。int *ret = getspace();//p把它的值直接給我ret,ret就是這塊內存,那么上邊的p就可以放心的釋放掉啦。(堆空間的話一定要拿到首地址,拿不到的話你就釋放不了啊)for (int i = 0; i < 5; ++i){printf("%d ",ret[i]);//打印}//用完堆內存一定要釋放free(ret);ret = NULL;//用完后就把指針置空就行啦 (因為后邊可能還有代碼,萬一后邊再用ret的話沒有置為空而是只是釋放掉了,那就直接down掉了,free空指針不會down掉,free野指針會down掉)
}//2、
void allocateSpace(char *p)//分配內存
{//定義變量的時候一定要初始化,因為很多bug的產生都是沒有初始化造成的(比如你寫個int a;但是沒有給a初始化,那么a+100你可以腦補一下是多少,不知道因為結果是不一定的,所以定義變量一定要初始化指針變量也是一定要初始化)。p = (char *)malloc(100);//申請100個字節memset(p, 0, 100);//初始化strcpy(p, "hello world");//拷貝(把常量區的數據hello world拷貝在字符空間里邊)
}
void test02()
{char *p = NULL;allocateSpace(p);printf("p=%s\n",p);
}
//修改
void allocateSpace02(char **p)//1、這里要寫兩個**
{char *temp = (char *)malloc(100);//來個局部變量char *tempmemset(temp, 0, 100);strcpy(temp, "hello world");*p = temp;//對指針解引用不要管他是幾級的,它指向的永遠是個內存空間,解引用相當于把這個內存空間給他放值啦,所以直接讓它等于temp。
}
void test022()
{char *p = NULL;allocateSpace02(&p);//2、因為這里要取地址(對一個指針取地址,相當于對應的是升級,就是加一個*,升*的話他應該就是一個二級指針,二級指針所以上邊1處參數類型要對等,所以他應該是2級的)printf("p=%s\n", p);
}int main()
{//test01();test022();system("pause");return 0;
}
那么從語法角度理解為什么非要寫兩個*呢?其實寫一個*也行, 我們知道不管幾級指針都占用4個字節,你把下邊allocateSpace02(&p);取完p的地址之后,傳進去void allocateSpace02(char **p)其實是一個一級指針,那我一級指針能不能畫一下這個地址值啊,當然可以,因為地址就占4個字節,為什么編譯器要設計出來兩個*的語法呢?便于如果這個地方不管是幾級指針你都用一級指針來接的話會有一個問題,當你看到這個指針的話你根本不知道這個指針指向的是個什么東西。畫圖說明一下修改的代碼:
先畫一個棧區,首先進入test022棧區放第一個變量p,分配4個字節的內存空間,值為空,比如這里有一個地址0x001,然后調用函數void allocateSpace02(char **p)里邊有一個p變量,這個p變量也要給他分配內存空間,指針變量不管幾級的不管幾個*都是4個字節,所以也是p,他也有內存地址0x002(這里的地址只是隨便取得),接下來執行到函數里邊,把allocateSpace02(&p);這個得p的值賦到void allocateSpace02(char **p)這個的p里邊,allocateSpace02(&p);我這里是取地址把0x001賦進來了把0x001賦到void allocateSpace02(char **p)這個里邊的p了,如果不取地址的話,賦的就是值,值才是空NULL,所以是把0x001放到這個空間里邊啦,如下:
??
然后緊接這往下執行有一個temp,假如temp我初始化的時候它是一個空,然后我再給它賦值,因為是malloc所以是在堆上,在堆上初始化100個字節,那么這個堆空間上100個字節,之后100個字節我們初始化全部變成0了,然后就是拷貝,拷貝也會有一個常量區。
初始完空間之后就把這個空間拷貝給temp這個變量,temp是一級指針占4個字節(temp是一個局部變量),temp的值是0x003,因為返回的是堆空間的地址
然后下邊就是初始化拷貝,hello word放在常量區。
然后再把字符串從常量區拷貝到堆上
然后就是執行這句*p = temp;*p是根據他找回地址去找這塊內存空間,p找回地址是0x001,*p的意思就是說我要找到地址為0x001這塊內存空間啊,(比如int a=10;意思是相當于把10放到a這塊內存空間里邊,所以*p就是你找一塊內存空間,等于一個數相當于把這個數放到這個內存空間里邊,(這里temp沒有取地址,所以就是temp的值,temp的值就是它內存空間上等于的值,把這個值給你剛才找到的首地址為0x001的這塊內存空間))
然后程序結束,這時要釋放內存空間并實現了指向。
?
?
總結
- 上一篇: C++:多线程中的小白(3)线程传参详解
- 下一篇: PCL:自定义创建带颜色的点云保存后rg