1. 程式人生 > >C語言記憶體分配問題和C語言中的記憶體

C語言記憶體分配問題和C語言中的記憶體

C語言記憶體分配問題

1、C中記憶體分為四個區
棧:用來存放函式的形參和函式內的區域性變數。由編譯器分配空間,在函式執行完後由編譯器自動釋放。 
堆:用來存放由動態分配函式(如malloc)分配的空間。是由程式設計師自己手動分配的,並且必須由程式設計師使用free釋放。如果忘記用free釋放,會導致所分配的空間一直佔著不放,導致記憶體洩露。 
全域性局:用來存放全域性變數和靜態變數。存在於程式的整個執行期間,是由編譯器分配和釋放的。 
文字常量區:例如char *c = “123456”;則”123456”為文字常量,存放於文字常量區。也由編譯器控制分配和釋放。 
程式程式碼區:用來存放程式的二進位制程式碼。
例子(一) 
int a = 0; //全域性區 
void main() 

int b; //棧 
char s[] = abc; //s在棧,abc在文字常量區 
char *p1,*p2; //棧 
char *p3 = 123456; //123456在常量區,p3在棧上 
static int c =0; //全域性區 
p1 = (char *)malloc(10); //p1在棧,分配的10位元組在堆 
p2 = (char *)malloc(20); //p2在棧,分配的20位元組在堆 
strcpy(p1, 123456); //123456放在常量區 

例子(二) 
//返回char型指標 
char *f() 

//s陣列存放於棧上 
char s[4] = {'1','2','3','0'}; 
return s; //返回s陣列的地址,但程式執行完s陣列就被釋放了 

void main() 

char *s; 
s = f(); 
printf (%s, s); //打印出來亂碼。因為s所指向地址已經沒有資料 

2、動態分配釋放記憶體
用malloc動態分配記憶體後一定要判斷一下分配是否成功,判斷指標的值是否為NULL。 
記憶體分配成功後要對記憶體單元進行初始化。 
記憶體分配成功且初始化後使用時別越界了。 
記憶體使用完後要用free(p)釋放,注意,釋放後,p的值是不會變的,仍然是一個地址值,仍然指向那塊記憶體區,只是這塊記憶體區的值變成垃圾了。為了防止後面繼續使用這塊記憶體,應在free(p)後,立即p=NULL,這樣後面如果要使用,判斷p是否為NULL時就會判斷出來。


NO.1
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str,hello world);
printf(str);
}
請問執行Test函式後會是什麼樣的結果?
NO.2
char *GetMemory(void)
{
char p[] = hello world;
retrun p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
問題同NO.1
NO.3
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str,100);
strcpy(str,hello);
printf(str);
}
問題同NO.1
NO.4
void Test(void)
{
char *str = (char *)malloc(100);
strcpy(str,hello);
free(str);
if(str != NULL)

strcpy(str,world);
printf(str);
}
}
問題同NO.1
我對以上問題的分析:
NO.1:程式首先申請一個char型別的指標str,並把str指向NULL(即str裡存的是NULL的地址,*str為NULL中的值為0),呼叫函式的過程中做了如下動作:1申請一個char 型別的指標p,2把str的內容copy到了p裡(這是引數傳遞過程中系統所做的),3為p指標申請了100個空間,4返回Test函式.最後程式把字串hello world拷貝到str指向的記憶體空間裡.到這裡錯誤出現了!str的空間始終為NULL而並沒有實際的空間.深刻理解函式呼叫的第2步,將不難發現問題所在!(建議:畫圖理解)
NO.2:程式首先申請一個char型別的指標str,並把str指向NULL.呼叫函式的過程中做了如下動作:1申請一陣列p[]並將其賦值為hello world(陣列的空間大小為12),2返回陣列名p付給str指標(即返回了陣列的首地址).那麼這樣就可以打印出字串"hello world"了麼?當然是不能的!因為在函式呼叫的時候漏掉了最後一步.也就是在第2步return陣列名後,函式呼叫還要進行一步操作,也就是釋放記憶體空間.當一個函式被呼叫結束後它會釋放掉它裡面所有的變數所佔用的空間.所以陣列空間被釋放掉了,也就是說str所指向的內容將不確定是什麼東西.
NO.3:正確答案為可以打印出hello.但記憶體洩漏了! 
NO.4:申請空間,拷貝字串,釋放空間.前三步操作都沒有任何問題.到if語句裡的判斷條件開始出錯了,因為一個指標被釋放之後其內容並不是NULL,而是一個不確定的值.所以if語句永遠都不能被執行.這也是著名的"野"指標問題.所以我們在編寫程式釋放一個指標之後一定要人為的將指標付成NULL.這樣就會避免出現"野"指標的出現.有人說"野"指標很可怕,會帶來意想不到的錯誤.

C語言記憶體對齊

C99規定int、unsigned int和bool可以作為位域型別,但編譯器幾乎都對此作了擴充套件,允許其它型別型別的存在。

使用位域的主要目的是壓縮儲存,其大致規則為:
1) 如果相鄰位域欄位的型別相同,且其位寬之和小於型別的sizeof大小,則後面的欄位將緊鄰前一個欄位儲存,直到不能容納為止;
2) 如果相鄰位域欄位的型別相同,但其位寬之和大於型別的sizeof大小,則後面的欄位將從新的儲存單元開始,其偏移量為其型別大小的整數倍;
3) 如果相鄰的位域欄位的型別不同,則各編譯器的具體實現有差異,VC6採取不壓縮方式,Dev-C++採取壓縮方式;
4) 如果位域欄位之間穿插著非位域欄位,則不進行壓縮;
5) 整個結構體的總大小為最寬基本型別成員大小的整數倍。

還是讓我們來看看例子。
示例1:
struct BF1
{
char f1 : 3;
char f2 : 4;
char f3 : 5;
};
其記憶體佈局為:
|_f1__|__f2__|_|____f3___|____|
|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|
0 3 7 8 1316
位域型別為char,第1個位元組僅能容納下f1和f2,所以f2被壓縮到第1個位元組中,而f3只能從下一個位元組開始。因此sizeof(BF1)的結果為2。
示例2:
struct BF2
{
char f1 : 3;
short f2 : 4;
char f3 : 5;
};
由於相鄰位域型別不同,在VC6中其sizeof為6,在Dev-C++中為2。
示例3:
struct BF3
{
char f1 : 3;
char f2;
char f3 : 5;
};

什麼是記憶體對齊

    考慮下面的結構:

         struct foo
         {
           char c1;
           short s;
           char c2;
           int i;
          };
    
    假設這個結構的成員在記憶體中是緊湊排列的,假設c1的地址是0,那麼s的地址就應該是1,c2的地址就是3,i的地址就是4。也就是
    c1 00000000, s 00000001, c2 00000003, i 00000004。

    可是,我們在Visual c/c++ 6中寫一個簡單的程式:

         struct foo a;
    printf("c1 %p, s %p, c2 %p, i %p/n",
        (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
    執行,輸出:
         c1 00000000, s 00000002, c2 00000004, i 00000008。

    為什麼會這樣?這就是記憶體對齊而導致的問題。

為什麼會有記憶體對齊

    以下內容節選自《Intel Architecture 32 Manual》。
    字,雙字,和四字在自然邊界上不需要在記憶體中對齊。(對字,雙字,和四字來說,自然邊界分別是偶數地址,可以被4整除的地址,和可以被8整除的地址。)
    無論如何,為了提高程式的效能,資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;然而,對齊的記憶體訪問僅需要一次訪問。
    一個字或雙字運算元跨越了4位元組邊界,或者一個四字運算元跨越了8位元組邊界,被認為是未對齊的,從而需要兩次匯流排週期來訪問記憶體。一個字起始地址是奇數但卻沒有跨越字邊界被認為是對齊的,能夠在一個匯流排週期中被訪問。
    某些操作雙四字的指令需要記憶體運算元在自然邊界上對齊。如果運算元沒有對齊,這些指令將會產生一個通用保護異常(#GP)。雙四字的自然邊界是能夠被16整除的地址。其他的操作雙四字的指令允許未對齊的訪問(不會產生通用保護異常),然而,需要額外的記憶體匯流排週期來訪問記憶體中未對齊的資料。

編譯器對記憶體對齊的處理

    預設情況下,c/c++編譯器預設將結構、棧中的成員資料進行記憶體對齊。因此,上面的程式輸出就變成了:
c1 00000000, s 00000002, c2 00000004, i 00000008。
編譯器將未對齊的成員向後移,將每一個都成員對齊到自然邊界上,從而也導致了整個結構的尺寸變大。儘管會犧牲一點空間(成員之間有空洞),但提高了效能。
也正是這個原因,我們不可以斷言sizeof(foo) == 8。在這個例子中,sizeof(foo) == 12。

如何避免記憶體對齊的影響

    那麼,能不能既達到提高效能的目的,又能節約一點空間呢?有一點小技巧可以使用。比如我們可以將上面的結構改成:

struct bar
{
    char c1; 
    char c2;
    short s;
    int i;
};
    這樣一來,每個成員都對齊在其自然邊界上,從而避免了編譯器自動對齊。在這個例子中,sizeof(bar) == 8。

    這個技巧有一個重要的作用,尤其是這個結構作為API的一部分提供給第三方開發使用的時候。第三方開發者可能將編譯器的預設對齊選項改變,從而造成這個結構在你的發行的DLL中使用某種對齊方式,而在第三方開發者哪裡卻使用另外一種對齊方式。這將會導致重大問題。
    比如,foo結構,我們的DLL使用預設對齊選項,對齊為
c1 00000000, s 00000002, c2 00000004, i 00000008,同時sizeof(foo) == 12。
而第三方將對齊選項關閉,導致
    c1 00000000, s 00000001, c2 00000003, i 00000004,同時sizeof(foo) == 8。

如何使用c/c++中的對齊選項

    vc6中的編譯選項有 /Zp[1|2|4|8|16] ,/Zp1表示以1位元組邊界對齊,相應的,/Zpn表示以n位元組邊界對齊。n位元組邊界對齊的意思是說,一個成員的地址必須安排在成員的尺寸的整數倍地址上或者是n的整數倍地址上,取它們中的最小值。也就是:
    min ( sizeof ( member ),  n)
    實際上,1位元組邊界對齊也就表示了結構成員之間沒有空洞。
    /Zpn選項是應用於整個工程的,影響所有的參與編譯的結構。
    要使用這個選項,可以在vc6中開啟工程屬性頁,c/c++頁,選擇Code Generation分類,在Struct member alignment可以選擇。

    要專門針對某些結構定義使用對齊選項,可以使用#pragma pack編譯指令。指令語法如下:
#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n  )
    意義和/Zpn選項相同。比如:

#pragma pack(1)
struct foo_pack
{
    char c1;
    short s;
    char c2;
    int i;
};
#pragma pack()

棧記憶體對齊

    我們可以觀察到,在vc6中棧的對齊方式不受結構成員對齊選項的影響。(本來就是兩碼事)。它總是保持對齊,而且對齊在4位元組邊界上。

驗證程式碼

#include <stdio.h>

struct foo
{
    char c1;
    short s;
    char c2;
    int i;
};

struct bar
{
    char c1; 
    char c2;
    short s;
    int i;
};

#pragma pack(1)
struct foo_pack
{
    char c1;
    short s;
    char c2;
    int i;
};
#pragma pack()


int main(int argc, char* argv[])
{
    char c1;
    short s;
    char c2;
    int i;

    struct foo a;
    struct bar b;
    struct foo_pack p;

    printf("stack c1 %p, s %p, c2 %p, i %p/n",
        (unsigned int)(void*)&c1 - (unsigned int)(void*)&i,
        (unsigned int)(void*)&s - (unsigned int)(void*)&i,
        (unsigned int)(void*)&c2 - (unsigned int)(void*)&i,
        (unsigned int)(void*)&i - (unsigned int)(void*)&i);

    printf("struct foo c1 %p, s %p, c2 %p, i %p/n",
        (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.i - (unsigned int)(void*)&a);

    printf("struct bar c1 %p, c2 %p, s %p, i %p/n",
        (unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,
        (unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,
        (unsigned int)(void*)&b.s - (unsigned int)(void*)&b,
        (unsigned int)(void*)&b.i - (unsigned int)(void*)&b);

    printf("struct foo_pack c1 %p, s %p, c2 %p, i %p/n",
        (unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,
        (unsigned int)(void*)&p.s - (unsigned int)(void*)&p,
        (unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,
        (unsigned int)(void*)&p.i - (unsigned int)(void*)&p);

    printf("sizeof foo is %d/n", sizeof(foo));
    printf("sizeof bar is %d/n", sizeof(bar));
    printf("sizeof foo_pack is %d/n", sizeof(foo_pack));
    
    return 0;
}