c語言的內存模型
所有C函數,只要其有返回值,都是由寄存器Ax(對於32位而言為EAX,如果AX不足以容納,則由DX保存超過AX容量的部分)保存的(也可能是值,也可能是某一地址,視返回類型而定)。
上面這個C函數程序,計算(x>y?x:y);後,該值就存在AX(EAX)中,當你在其他函數中調用該函數
後,比如
a=max(m,n);後,其實就是進入max()完成後,函數返時將計算結果存放在AX中,執行這個語句後,即就將AX的值傳給了a.
其實,這也很容易驗證:
在調用
a=max(m,n);
緊接著輸入以下語句(注意:這中間絕對不能有其他任何函數調用和語句):
b=_AX;
printf("a=%d,b=%d",a,b);
你就可能發現,a=和b=一定是同一個值。
❷ C語言代碼組成 - BSS、Data、Stack、Heap、Code、Const
一段C語言經過編譯連接後,成為一段可以運行的代碼,可運行的代碼可以分為以下四個部分組成:全局變數/靜態變數區、堆、棧、代碼區。其中全局變數/靜態變數區又分為未初始化變數區和初始化變數區,代碼區又分為代碼和常量區。即匯總下來,代碼可以分為6部分組成,包括:BSS區(未初始化的全局變數/靜態變數區)、Data區(實始化的全局變數區)、Stack區(棧區)、heap區(堆區)、Code區(代碼區)、const區(常量區)。
一、BSS區和Data區
C語言編程中定義的全局變數、靜態局部變數,就是分配在全局變數/靜態變數區域,但是為什麼又要分為BSS區域和Data區域呢?其實我們在定義全局或者靜態變數區,有時我會對它賦初始值,有的又不會賦初始化,比如我們定義的全局變數,初始化的賦值,是怎麼樣寫到變數區域中的,我們定義的靜態局部變數,在定義時初始化後,為什麼後面函數被調用,又不會再初始化呢?這個局部靜態變數是怎麼樣實始化的,什麼時候初始化的?
如果分析編譯後的匯編代碼,就會發現在代碼運行起來後,會有一段給變數賦值的指令,這一段代碼,不是我們C代碼對應的匯編,而是C編譯器生成的匯編譯代碼,這段代碼的作用就是給初始化了的靜態變數和全局變數進行初始化。這也是為什麼全局/靜態變數區域,要分BSS和Data的原因。
二、Stack區
棧是一種先進後出的數據結構,這種數據結構正好完美的匹配函數調用時的模型過程,比如函數f(a)在運行過程中調用函數f(b),f(a)在運行過程中的變數就是分配在棧中,通過在調用f(b)前,會將代碼中用到的R0~Rn寄存器的值保存到棧中,同時將函數的傳入參數寫入到棧中,然後進入f(b)函數,函數f(b)的變數b分配在棧中,當函數運行完畢後,釋放變數b,將棧中存放的f(a)函數的運行的R0~Rn寄存器值恢復到寄存器中,同時f(b)的返回結果存入到棧中,這樣f(a)繼續運行。當一個函數運行完畢後,它在棧中分配的臨時變數會全部釋放。
對於中斷也是一樣的,中斷發生時,也是一個函數打斷了另一個函數的運行,這種現場的保存(即寄存器的值),都是通過棧來完成的。所以棧的作用有:
三、Heap區
全局變數分配的內存在代碼整個運行周期內都是有效的,而在棧區分配的內存在函數調用完成後,就會釋放。這兩種內存模型都是由編譯器決定它的使用,代碼是無法控制的。那有沒有內存是由用戶控制的,要用時,就自由分配,不用時,就自行釋放?答案是肯定的,這部分內存就是堆。
用戶需要使用的動態內存,就是通過malloc函數,調用分配的,在沒有釋放前,可一直由代碼使用。當這部分內存不再需要使用時,可以通過free函數進行釋放,將它歸還到堆中。從這中可以看出,堆的內存,是按需分配的。這就是賦予了代碼很大的自由度,但這也是會帶來負作用的,比如:內存碎片化導致的malloc失敗;忘記釋放內存導致的內存泄露,而這些往往是致命的失誤。
四、Code區
代碼區就是編譯後機器指令,這些指令決定了功能的執行。我們編譯的代碼一般是下載進flash中,但是運行,卻有兩種方式:在RAM中運行和在ROM中運行。 在RAM中運行,即是boot啟動後,將flash中的代碼復制到RAM中,然後PC指針在指到RAM中的代碼中開始運行。 有時在調試時,我們可以直接將代碼下載進RAM中運行進行調試,這樣加快調試速度。便是大部分的情況我們的代碼是從flash中開始運行的。
五、常量區
代碼中的常量,一部分是作為立即數,在代碼區中,但是像定義的字元串、給某數組賦值的一串數值,這些常量,就存在常量區,我們常用const來定義一個常量,即該變數不能再必變。這部分的變數,編譯器一般將它定義的flash中。
六、各個區域大小的是如何決定的:
code區和const區:是由代碼的大小和代碼中常量的多少來決定的。
bss區和data區:這是由代碼中定義的全局變數和局部變數的多少來決定的。
stack區:這個可以由使用都自行定義大小,但使用都要根據自已代碼的情況,評估出一個合理的值,再定義其大小,如果定義的太小,很容易爆棧,導至代碼異常,但是如果定義的太大,就容易浪費內存。
heap區:RAM剩下的部分,編譯器就會作為堆區使用。
七、嵌入式代碼一般啟動過程
以STM32為例,通過分析其匯編啟支代碼,大致可以分為以下幾個步驟:
如果大家想看編譯扣,代碼文件的組成,可以查看統後生的map文件,裡面有詳細的數據,包括各個函數的分配內存,BSS,Data,Stack,Heap,Text的分配情況。
如果相要了解詳細的代碼啟動過程,可看它的啟動匯編文件。
❸ 關於C語言中數組作為參數傳遞的疑惑~~
湊湊熱鬧,同意terry_tang的觀點,另做些補充:
先看代碼:
#include <stdio.h>段謹鏈
void foo(int array[2]){printf("int array[2]:\t\t%x %d\n", &array, sizeof(array));}
void bar(int array[]){printf("int array[]:\t\t%x %d\n", &array, sizeof(array));}
void baz(int (&array)[2]){printf("int (&array)[2]:\t%x %d\n", &array, sizeof(array));}
int main()
{
int a[2] = {1, 2};
printf("main::a[2]:\t\t%x %d\n", a, sizeof(a));
foo(a);
bar(a);
baz(a);
return 0;
}
解說:
foo和bar的傳值方式是相同的,都是一個int*, 即一個整型指針,這可以從foo和bar里列印出的array地址和main中的不同和sizeof(array)僅為sizeof(int*)看出,只不過是外型有點兒區別。
編譯器是不知道你要傳遞的是一個數組或是單一一個整型的地址的,這是因為C中數組的內存模型是連續存儲(它並不知道傳遞的(首)地址之後的空間可否訪問)。
所以寫為foo或bar的樣式僅僅是對人的一種暗示,暗示傳遞的是一個數組晌哪,括弧里的2編譯器是不會把他當回事兒的①。
採用foo中的樣式,代碼編寫者在函數中獲知傳遞的數組的大小,但這種暗示功能很弱,而且易使人產生誤解。
比如以上的函數foo,傳遞大小為1個元素的數組(即單一一個整型的地址):
int x[1];
foo(x);
或傳遞一個大小為100的數組:
int x[100];
foo(x);
編譯器都不會有任何抱怨,所以在代碼工程量很大的時候,你無法保證數組傳值的安全性,另外一個問題是如果你寫的是商業性質的庫,你無法保證客戶(二次開發者)能安全地使用你的代碼。
採用bar中樣式,實質和foo相同,空括弧給人的暗示就是它能接受的參數是一個數組,而且是一個長度不確定的一維整型數組,這相對於foo來說更為實際和真實一些(因為foo可能造成欺騙性的代碼,原因見上)。
所以這種傳數組的方式被多數人所採用,但一般還需多加一個參數來指定數組的大小,如:
void bar(int array[], int size);
或效仿STL的做法,傳遞數組的首地址和超尾指針(在遍歷數組元素時很方便,且更快速、安全):
void bar(int* beg, int* end);
至於baz,它不同於foo和bar。前面已經說過,foo和bar實質是相同的,傳的都是一個int*,且傳值方式都是按值傳遞(C中只有按值傳遞)。
而baz卻是按引用傳遞,傳遞的是一個"編譯器認可的,大小為2"②的數組的引用。
foo和bar都可以改寫為:
void theFact(int* array);
或
void theFact(int* array, int size);
按照此邏輯是不是baz可改寫為這樣呢?
void baz2(int* const& array); // a其實是一個int* const型指針,所以要加上const作為修飾
答案是否定的,注意上面的②,只有在C++中,函數"按引用"傳遞數組並"指定其大小時",[]中的數字才有意義(對編譯器而言)。所以baz2 != baz:
int x[100];
baz(x); // 編譯錯誤
baz2(x); // 可以通過
要理解這和foo, bar的不同首先要理解C++中對引用的定義: 引用就是對象本身,不存在沒有引用對象的引用。所以在baz中,形參array就是實參main中的a,一切a所有的特性都是array的特性,所以sizeof(array) == sizeof(a),而且&baz::array == main::a(地址相同)。
①: C99中允許使握孫用static數組參數修飾詞,如:
void foo(int x[static 10]); // x數組至少含有10個連續元素
上句中的10此時並不是可有可無的,它是編譯器優化數組訪問的一種暗示。
❹ C語言重新賦值內存地址會變嗎
變數a的地址是不能變的,當程序被載入時,操作系統會為它分配好地址,且一經分配,不能再改變!當然,每一次程序運行時,操作系統為a分配的地址可以不同。
有以下幾點常常另初學者感到困惑,現舉例加以解釋:
int b,c,*a;
a=&b;
語句a=&b;並沒有改變a的地址,它只是改變了a這個箱子中裝的東西。如果你在語句a=&b;的前後用printf("%d",(int)&a);輸出a的地址,就會發現它們是一樣的。如果後面再來個a=&c;則printf("%d",(int)&a);的輸出也一樣!
關鍵是要區分以下幾點:
a,表示a的值,即它裝的東西,具體到這個例子,a裝的是另一個int型變數的地址。如果a不是const類型的,則它裝的東西可以改變。比如,這里先裝的是b的地址(指針變數是用來裝地址的),後改成了c的。
&a,當然就表示a自己的地址了,你可以將a想像成一個箱子,它的地址就是這個箱子的編號。
*a,因為a是一個指針,*a就表示a指向的變數的值,即b或c的值(具體要看a裝的是誰的地址,即a指向誰),也即*a=b或*a=c。
現假設a裝的是b的地址,那有:
a=&b,即a的值等於b的地址。
*a=b=*(&b),這里*的作用是取出某個地址中的值。因為a的值是b的地址,因此*a取出的是b的值,同理(&b)是b的地址,*(&b)取出的也是b的值!
分析一下a,b,c的內存模型(即它們在內存中是怎樣表示的、關系又是怎樣的),理解這些就不難了,你邊學邊體會吧!
❺ C語言怎麼寫底層.這是什麼情況
C語言的內存模型基本上對應了現在von Neumann(馮.諾伊曼)計算機的實際存儲模型,很好的達到了對機器的映射,這是C/C++適合做底層開發的主要原因,另外,C語言適合做底層開發還有另外一個原因,那就是C語言對底層操作做了很多的的支持,提供了很多比較底層的功能。
下面結合問題分別進行闡述。
問題:移位操作
在運用移位操作符時,有兩個問題必須要清楚:
(1)、在右移操作中,騰空位是填 0 還是符號位;
(2)、什麼數可以作移位的位數。
答案與分析:
">>"和"<<"是指將變數中的每一位向右或向左移動, 其通常形式為:
右移: 變數名>>移位的位數
左移: 變數名<<移位的位數
經過移位後, 一端的位被"擠掉",而另一端空出的位以0 填補,在C語言中的移位不是循環移動的。
(1) 第一個問題的答案很簡單,但要根據不同的情況而定。如果被移位的是無符號數,則填 0 。如果是有符號數,那麼可能填 0 或符號位。如果你想解決右移操作中騰空位的填充問題,就把變數聲明為無符號型,這樣騰空位會被置 0。
(2) 第二個問題的答案也很簡單:如果移動 n 位,那麼移位的位數要不小於 0 ,並且一定要小於 n 。這樣就不會在一次操作中把所有數據都移走。
比如,如果整型數據占 32 位,n 是一整型數據,則 n << 31 和 n << 0 都合法,而 n << 32 和 n << -1 都不合法。
注意即使騰空位填符號位,有符號整數的右移也不相當與除以 。為了證明這一點,我們可以想一下 -1 >> 1 不可能為 0 。
問題:位段結構
struct RPR_ATD_TLV_HEADER
{
ULONG res1:6;
ULONG type:10;
ULONG res1:6;
ULONG length:10;
};
位段結構是一種特殊的結構, 在需按位訪問一個位元組或字的多個位時, 位結構比按位運算符更加方便。
位結構定義的一般形式為:
struct位結構名{
數據類型 變數名: 整型常數;
數據類型 變數名: 整型常數;
} 位結構變數;
其中: 整型常數必須是非負的整數, 范圍是0~15, 表示二進制位的個數, 即表示有多少位。
變數名是選擇項, 可以不命名, 這樣規定是為了排列需要。
例如: 下面定義了一個位結構。
struct{
unsigned incon: 8; /*incon佔用低位元組的0~7共8位*/
unsigned txcolor: 4;/*txcolor佔用高位元組的0~3位共4位*/
unsigned bgcolor: 3;/*bgcolor佔用高位元組的4~6位共3位*/
unsigned blink: 1; /*blink佔用高位元組的第7位*/
}ch;
位結構爛穗猛成員的訪問與結構成員的訪問相同。
例如: 訪問上例飢橋位結構中的bgcolor成員可寫成:
ch.bgcolor
位結構成員可以與其它結構成員一起使用。 按位訪問與設置,方便&節省
例如:
struct info{
char name[8];
int age;
struct addr address;
float pay;
unsigned state: 1;
unsigned pay: 1;
}workers;'
上例的結構定義了關於一個工從的信息。其中有兩個位結構成員, 每個位結構成員只有一位, 因此只佔一個位元組但保存了兩個信息, 該位元組中第一位表示工人的狀態, 第二位表示工資是否已發放。由此可見使用位結構可以節省存貯空間。
注意不要超過值限制
問題:位元組對齊
我在使用VC編程的過程中,有一次調用DLL中定義的結構時,發覺結構都亂掉了,完全不能讀取正確的值,後來發現這是因為DLL和調用程序使用的位元組對齊選項不同,那麼我想問一下,位元組對齊究竟是怎麼一回事?
答案與分析:
關於位元組對齊:
1、 當不同的族歲結構使用不同的位元組對齊定義時,可能導致它們之間交互變得很困難。
2、 在跨CPU進行通信時,可以使用位元組對齊來保證唯一性,諸如通訊協議、寫驅動程序時候寄存器的結構等。
三種對齊方式:
1、 自然對齊方式(Natural Alignment):與該數據類型的大小相等。
2、 指定對齊方式 :
#pragma pack(8) //指定Align為 8;
#pragma pack() //恢復到原先值
3、 實際對齊方式:
Actual Align = min ( Order Align, Natual Align )
對於復雜數據類型(比如結構等):實際對齊方式是其成員最大的實際對齊方式:
Actual Align = max( Actual align1,2,3,…)
編譯器的填充規律:
1、 成員為成員Actual Align的整數倍,在前面加Padding。
成員Actual Align = min( 結構Actual Align,設定對齊方式)
2、 結構為結構Actual Align的整數倍,在後面加Padding.
例子分析:
#pragma pack(8) //指定Align為 8
struct STest1
{
char ch1;
long lo1;
char ch2;
} test1;
#pragma pack()
現在
Align of STest1 = 4 , sizeof STest1 = 12 ( 4 * 3 )
test1在內存中的排列如下( FF 為 padding ):
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF FF FF 01 01 01 01 01 FF FF FF
ch1 -- lo1 -- ch2
#pragma pack(2) //指定Align為 2
struct STest2
{
char ch3;
STest1 test;
} test2;
#pragma pack()
現在 Align of STest1 = 2, Align of STest2 = 2 , sizeof STest2 = 14 ( 7 * 2 )
test2在內存中的排列如下:
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
02 FF 01 FF FF FF 01 01 01 01 01 FF FF FF
ch3 ch1 -- lo1 -- ch2
注意事項:
1、 這樣一來,編譯器無法為特定平台做優化,如果效率非常重要,就盡量不要使用#pragma pack,如果必須使用,也最好僅在需要的地方進行設置。
2、 需要加pack的地方一定要在定義結構的頭文件中加,不要依賴命令行選項,因為如果很多人使用該頭文件,並不是每個人都知道應該pack。這特別表現在為別人開發庫文件時,如果一個庫函數使用了struct作為其參數,當調用者與庫文件開發者使用不同的pack時,就會造成錯誤,而且該類錯誤很不好查。
3、 在VC及BC提供的頭文件中,除了能正好對齊在四位元組上的結構外,都加了pack,否則我們編的Windows程序哪一個也不會正常運行。
4、 在 #pragma pack(n) 後一定不要include其他頭文件,若包含的頭文件中改變了align值,將產生非預期結果。
5、 不要多人同時定義一個數據結構。這樣可以保證一致的pack值。 問題:按位運算符
C語言和其它高級語言不同的是它完全支持按位運算符。這與匯編語言的位操作有些相似。 C中按位運算符列出如下:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
操作符 作用
————————————————————————————
& 位邏輯與
位邏輯或
^ 位邏輯異或
- 位邏輯反
>> 右移
<< 左移
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
注意:
1、 按位運算是對位元組或字中的實際位進行檢測、設置或移位, 它只適用於字元型和整數型變數以及它們的變體, 對其它數據類型不適用。
2、 關系運算和邏輯運算表達式的結果只能是1或0。 而按位運算的結果可以取0或1以外的值。 要注意區別按位運算符和邏輯運算符的不同, 例如, 若x=7, 則x&&8 的值為真(兩個非零值相與仍為非零), 而x&8的值為0。
3、 與 ,&與&&,~與! 的關系
&、 和 ~ 操作符把它們的操作數當作一個為序列,按位單獨進行操作。比如:10 & 12 = 8,這是因為"&"操作符把 10 和 12 當作二進制描述 1010 和 1100 ,所以只有當兩個操作數的相同位同時為 1 時,產生的結果中相應位才為 1 。同理,10 12 = 14 ( 1110 ),通過補碼運算,~10 = -11 ( 11...110101 )。<以多少為一個位序列> &&、 和!操作符把它們的操作數當作"真"或"假",並且用 0 代表"假",任何非 0 值被認為是"真"。它們返回 1 代表"真",0 代表"假",對於"&&"和""操作符,如果左側的操作數的值就可以決定表達式的值,它們根本就不去計算右側的操作數。所以,!10 是 0 ,因為 10 非 0 ;10 && 12 是 1 ,因為 10 和 12 均非 0 ;10 12也是 1 ,因為 10 非 0 。並且,在最後一個表達式中,12 根本就沒被計算,在表達式 10 f( ) 中也是如此。
❻ c語言指針在什麼情況下需要malloc,什麼時候可以不這樣,(被這個問題搞暈了)
C語言的內存模型包含有棧和堆兩部分。
棧存放的是函數執行時答圓的變燃尺量等,這部分變數由系統自動管理,比如 int a;等,這些變數在函數體結束時自動收回。
堆存放的是由用戶自己手動管理的變數,這些變數是的由malloc函數建立,由 free函數釋放。不皮舉高會在函數體結束時自動收回。
❼ C語言和C++的內存模型一樣嗎
堆棧是一樣的。這些細節編程的人敏歲不用管。new出來的空間是自動清襪管理的,編橋正睜程者不用操心。malloc是編程人自己直接管理空間的,你索要了空間,最後要歸還。因為系統不知道你什麼時候不需要你申請的空間了。
