C語言學習筆記(四)——內存管理

開始今天的課程吧ヾ(◍°∇°◍)ノ゙

1.作用域

#include<stdio.h>
int a=2;//文件作用域
int main()
{
    int a = 0;//函數作用域
    {
        int a=1;//代碼塊作用域
        printf("a=%d/n,a");//現在輸出1,代碼變量會把函數變量和全局變量屏蔽
    }
    printf("a=%d/n,a");//此時輸出a=0,若將函數作用域註釋掉,輸出a=2,再註釋掉文件作用域就會報錯(局部變量全局變量同名,全局會被屏蔽掉)
}
//不同 作用域的變量名稱可以相同

自動變量與靜態變量

首先是自動變量:

int main()
{
    auto signed int a=0;//在代碼塊中,直接寫int會自動默認auto,自動出現在內存中,一般都省略了
    register int b = 0;//出現在寄存器中
    static int c = 0;//靜態變量是指內存位置在程序執行期間一直不改變的變量。一個代碼塊內部的靜態變量只能被這個代碼內部訪問,靜態變量是程序剛加載到內存就出現,所以和定義靜態變量的大括號無關,一直到程序結束才從程序消失,同時靜態變量的值只初始化一次
}

下面寫一個例子:

int test()
{
int a=0;
static b=0;
a++;
b++;
printf("%d,%d/n",a,b); 
}
int main ()
{
    for(i=0;i<10;i++)
    test();//此時輸出的a值全爲1。而b的值爲12345...因爲每次test的大括號內容執行完畢,a的值就會消失(默認auto),但是靜態變量的值和大括號是否執行完畢無關,且只初始化一次,因此值一直增加。
}

下面來看代碼塊之外的靜態變量

#include<stdio.h>
int b = 0;//全局變量
static int a = 0; //全局變量的存儲方式和靜態變量相同,但可以被多個文件訪問,定義在代碼塊之外的變量就是全局變量,全局變量即使不在一個文件中也不能重名,可以在另一個文件中用extern int a聲明,編譯的時候一起編譯就行(對於文件外的函數也可以用extern聲明,例如extern test();可以省略extern()。沒有static所有的函數默認是全局的。代碼塊之外的靜態全局變量只能在定義它的文件內部訪問,對於外部的其他文件是不可使用的(函數前加static,函數是靜態函數,也不能在文件外被調用)。C語言默認寫在代碼塊的函數或變量都是全局的。C語言中extern int b;//明確聲明瞭一個變量,一定不是定義變量。int b ;//如果這個變量定義過了,這裏就代表聲明,如果沒定義過這裏就是定義。

int main()
{
    printf("a=%d\n",a);
    return 0;
}

2.代碼區的靜態區與棧區

代碼區code,程序被操作系統加載到內存的時候,所有的可執行代碼加載到代碼區,也叫代碼段,這段內存不能在運行期間修改,只能執行。
靜態區是程序加載到內存的時候就確定了,程序退出的時候從內存消失。所有的全局變量和靜態變量在程序運行期間都佔用內存。
棧stack是一種先進後出的內存結構,所有的自動變量,函數的形參,函數的返回值都是由編譯器自動放出棧中,當一個自動變量超出其作用域時(大括號),自動從棧中彈出。下面是個例子

#include<stdio.h>
void test (int n)
{
    printf("%p,n=%d\n",&n,n);
    if (n<10)
    test(n+1);
    printf("%p,n=%d\n",&n,n);

}
int main ()
{
    test (0);
    return 0;
}

結果輸出如下(注意觀察地址):
這裏寫圖片描述

不同的系統棧的大小是不一樣的,即使相同的系統,棧的大小也是不一樣的,windows程序在編譯的時候就可以指定棧的大小,linux棧的大小是可以通過環境變量設置的。
堆heap和棧一樣,也是一種在程序運行中而已隨時修改的內存區域,但沒有棧那樣的先進後出的順序。
堆是一個大容器,它的容量要遠遠大於棧,但是在C語言中,堆內存空間的申請和釋放需要手動通過代碼來完成。

#inchude<stdio.h>
int a=0;
static int b = 1;

int main()
{
    static int c = 2;
    int d = 3;
    int d = 4;
    printf("%p,%p,%p,%p,%p",&a,&b,&c,&d,&e);
    //輸出的abc 在一塊,d不在一塊 d e 在一起,都在棧中。

}

3.堆的分配和釋放

1malloc

void * malloc(size_t _Size);

需要

#include<stdio.h>
#inchude<stdlib.h>
int main()
{
    char *s = malloc(10);//在對重分配了10個字節的空間
    一個棧裏面的自動指針變量指向了一個堆地址空間
    strcpy(s,"abcd");
    printf("%s\n");
    free(s);
    s=malloc(100);//因爲s是自動變量(auto),可以重新使用,這個時候重新又指向了一個新的堆空間
    free(s);//free(s)並不是把變量s釋放了,而是釋放了s指向的那塊內存空間
    int a[10];//定義了一個數組,這個數組在內存的棧裏面。
    一個程序的棧大小是有限的,如果一個數組特別大,會導致棧溢出,所以在程序中不要用太大的數組(具體多大不確定,超出範圍會報錯)。如果一個數組定義的時候大小不能確定,適合用堆不適合用棧。
    int *p=malloc(100000000*sizeof(int));//使用這種方法防止棧溢出
    free(p);//記得free
    return 0;
}

動態變化大小的數組示例:

int main()
{
    int i;
    scanf("%d",&i);
    int *p=malloc(i*sizeofint());
    int a ;
    for(a=0;a<i;a++)
    {
        printf("%d\n",p[a]);//可以把這個指針默認爲一個數組。
    }
    free(p);//如果malloc沒有free,那麼叫內存泄露,是非常容易犯的錯誤。
}

1.1函數返回一個指針

#include<stdio.h>
#include<stdlib.h>
int *test()
{
    int a = 10;//a是一個auto變量,在棧裏面
    return &a;
}
int *test1()
{
int *p = malloc(1*sizeof(int));
*p=10;
return p;
}
char *test2()
{
//char a[100]="hello";//函數不能直接返回一個auto類型的地址
char *a = malloc(100);
strcpy(a,"hello");
return a ;
}
char *tesr3(char *arg)
{
    retuan arg;
}
char *test4(char *arg)
{
    return &arg[5];
}
char *test5()
{
    char *p=malloc(100);
    *p='a';
    //*(p+1)='b';
    //*(p+2)=0;//最終輸出ab沒有問題
    p++;
    *p='b';
    p++;//這個時候會報錯,因爲在此過程中p本身的值發生改變了,不再指向首地址了,那麼free(p)向後釋放100個字節內存時,向後多釋放了2個字節(沒分配的空間被釋放了)。
    *p=0;

}
int main()
{
    //int *p = test();//test內部的變量a已經不在內存了,所以p指向一個無效的空間(會warning,但是還是能打印出a的值)
    int *p = test1();//不會warning,記得free
    char *p1 = test2();//此時,a是一個局部的變量(auto),調用完之後在內存消失了。p1指向無效的內存,所以是一個野指針。所以需要用堆
    char a [100]="hahaheiheiqieqie";
    p2=test3(a);//沒有問題
    p3=test4(a);//沒有問題
    printf("%d,%s,%s,%s\n",p,p1,p2,p3);
    free(p);
    free(p1);
    return 0;
}

很有很多返回值爲指針的情況,寫在一起太混亂了,額外寫一個程序吧

include<stdio.h>

char *test()
{
    static char a[100]="hello";//靜態的變量,在靜態區,所以可用
    char *p=a;
    p++;
    //return p;//此時的輸出是有效的、
    return a;
}
const char *test1()
{
    const char *s ="hello";//意思是將s指向一個常量的地址,常量在程序運行期間是一直有效的。
    return s;
}
const char *test2()//如果把const去掉,就是把一個常量的地址給一個變量的地址,函數定義的地址和返回的地址類型不符(修改這個常量的值,不會報錯,但是運行會出錯),常量區靜態區類似,程序運行期間有效,但常量區是隻讀的。
{
    return "hello world";//和test1一樣是合法的沒有問題,返回的是一個常量的地址。
}
int main()
{
    char *str = test();
    const char *str1 = test1();

    printf("%s%s\n",str,str1);
    return 0;
}

2free

void free(void *p);

free負責在堆中釋放malloc分配的內存。參數p爲malloc返回的堆中的內存地址

3calloc:

void * calloc(size_t _Count, size_t _Size);

calloc與malloc類似,負責在堆中分配內存。Malloc只分配,但不負責清理內存,
calloc分配內存的同時把內存清空
第一個參數是所需內存單元數量,第二個參數是每個內存單元的大小(單位:字節),calloc自動將分配的內存置0
int p = (int )calloc(100, sizeof(int));//分配100個int
用malloc分配的內存,用memset清空,用calloc分配的不需要額外清空。
這四個函數都在

4realloc

重新分配用malloc或者calloc函數在堆中分配內存空間的大小。

void * realloc(void *p, size_t _NewSize);

第一個參數 p爲之前用malloc或者calloc分配的內存地址,_NewSize爲重新分配內存的大小,單位:字節。其實可以用malloc實現,但是realloc如果當前的空前拓展時,後面的空間被佔用了,無法拓展時,它會自動找一個新的空間,然後自動釋放原空間,比較方便智能。
成功返回新分配的堆內存地址,失敗返回NULL.
如果參數p等於NULL,那麼realloc與malloc功能一致
Realloc不會自動清理增加的內存,需要手動清理,如果指定的地址後面有連續的空間,那麼就會在已有地址基礎上增加內存,如果指定的地址後面沒有空間,那麼realloc會重新分配新的連續內存,把舊內存的值拷貝到新內存,同時釋放舊內存。

4.堆棧的使用

例如兩個字符串操作strcat,兩個字符串長度不確定,此時用棧無法滿足,可以用堆,用malloc(strlen(a)+strlen(b)+1) 不用sizeof,+1是爲了放最後的0。記得free
小總結(把課程PPT寫到這裏算一個記錄吧)
論空間分配速度:
棧區速度要快於堆區。
使用棧時,是直接從地址讀取數據到寄存器,然後放到目標地址;
使用堆時,第一步將分配的地址放到寄存器,然後取出這個地址的值,然後放到目標地址。大概是這樣,堆的數據讀出要多一

論空間訪問速度:
    對於CPU來說是一樣的,都是一個直接尋址過程。

1.每個線程都有自己專屬的棧(stack),先進後出(LIFO)
2.棧的最大尺寸固定,超出則引起棧溢出
3.變量離開作用範圍後,棧上的數據會自動釋放
4.堆上內存必須手工釋放(C/C++),除非語言執行環境支持GC
棧還是堆?
明確知道數據佔用多少內存
數據很小
大量內存
不確定需要多少內存

Code Area:程序代碼指令、常量字符串,只可讀
Static Area:存放全局變量/常量、靜態變量/常量
Heap:由程序員控制,使用malloc/free來操作
Stack:預先設定大小,自動分配與釋放
堆棧的大小是動態變化的,靜態區程序加載後就不變了
這裏寫圖片描述
下面是棧中常犯的錯誤

const char test()
{
    const char a[]="hello";//錯誤的,即使限定數組a是個常量,但它始終處於棧中,大括號結束,地址就無效了。
    return a ;
}
int main ()
{
    const char *s =test();
    printf ("%s\n",s);
    return 0;
}
const char test()
{
    static char a[]="hello";//數組a在靜態區裏面,地址是一直有效的。
    return a ;
}
int main ()
{
    const char *s =test();
    printf ("%s\n",s);
    return 0;
}

對於如下代碼:
這裏寫圖片描述
可以得到在堆棧內存的映射關係
這裏寫圖片描述
注意list_buf變量本身在棧中,但是用malloc分配的空間是在堆中
再看一個代碼
這裏寫圖片描述
這裏寫圖片描述

棧的原理:
這裏寫圖片描述
棧頂從高地址向低地址方向增長
存儲非靜態局部變量、函數參數、返回地址
int abc(int a,int b){}//c語言形參是從右到左入棧的,先入棧b再入棧a (其他語言不一定支持)

再用下面的代碼和圖分析一下棧
這裏寫圖片描述
首先可以看到數組a放在靜態區
這裏寫圖片描述
這裏寫圖片描述
注意free(b)只是釋放了在堆中的那段內存,b本身還是在棧中的。

5.通過指針形參分配堆內存的說明

include<stdio.h>
include<stdlib.h>
include<string.h>
viod test (char *s)
{

    strcpy(s,"hello");
}
int main()
{
    char *p = calloc(10,1);//堆中分配了10個char 這樣的執行結果是正確的

    test(p);
    printf("%s\n",p);
    free(p);
    return 0;
}
int main()
{
    test("hello");//錯誤,相當於在棧裏面有i="hello",是個常量,不能改變
    return 0;
}

那麼換一種方法是否還是正確的呢?

include<stdio.h>
include<stdlib.h>
include<string.h>
viod test (char *s)
{
    s = calloc(10,1); 
    strcpy(s,"hello");
}
int main()
{
    //char *p = calloc(10,1);//堆中分配了10個char 這樣的執行結果是正確的
    char *p = NULL;
    test(p);
    printf("%s\n",p);
    free(p);
    return 0;
}

上述的代碼是特別容易犯的錯誤。
這裏寫圖片描述
代碼在調用函數時,p的值並沒有改變,p依然是空的,那麼首先printf無法輸出,而free(p)也free一個空的指針,而s的內存在執行完就泄露了,而s本身在調用完之後就不見了,再也無法找到這段內存。
那麼如何對上面的代碼改進呢?使用下面的代碼,改變了指針的級數,改成了二級指針。

include<stdio.h>
include<stdlib.h>
include<string.h>
viod test (char **s)
{
    *s = calloc(10,1); 
    strcpy(*s,"hello");//strcpy的第一個參數是指針,所以用*s
}
int main()
{
    //char *p = calloc(10,1);//堆中分配了10個char 這樣的執行結果是正確的
    char *p = NULL;
    test(&p);
    printf("%s\n",p);
    free(p);
    return 0;
}

爲什麼上述的代碼就可以用了呢?可以參見下圖:
s的值爲0x123,然後當分配一段堆內存後,*s就代表0x123這塊內存的值,此時,p就由空變成了0x100成功指向了堆。
這裏寫圖片描述

操作系統分配內存的最小單位說明
在windows系統下

#include <stdio.h>
#include <stdlib.h>
#pragma warning(disable:4996)//在VS中防止scanf等warning
int main()
{
    while (1)
    {
        char *s = malloc(1024);
        getchar();//執行到這裏暫停一下
    }
    return 0;
}

編譯運行上述程序,在任務管理器中找到它,按四次回車,內存值纔會變化,從任務管理器可以發現它佔用的內存爲504 508 512 516 520,也就是每次堆的變化是4K,如果需要1k空間,操作系統會給4K,如果要5K,操作系統會給8k;
4K就是內存最小頁。減少了內存的操作,效率增加,只是會浪費一些內存。
在實際程序中,malloc(1024)和malloc(4*1024)佔用的內存是一樣的,寫成後面這種可以提高效率

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章