C語言內存管理

在 C 語言中,當一個程序被加載到內存中運行,系統會爲該程序分配一塊獨立的內存空間,而且這塊內存空間又能夠再被細分爲不少區域,好比:棧區、堆區、靜態區、全局區......等。這裏只介紹經常使用的內存區域:棧區、堆區。程序員

(一) 棧區與堆區

棧區:保存局部變量。存儲在棧區的變量,在函數執行結束後,會被系統自動釋放。
堆區:由 malloc、calloc、realloc……等函數分配內存。其生命週期由 free 函數控制,在沒有被釋放以前一直存在,直到程序運行結束。數組

1. 棧內存

定義在函數內部的局部變量,都保存在棧區。棧區的特色是:函數執行結束後,由系統「自動回收」局部變量所對應的內存空間。所謂的「自動回收」實際上是操做系統將這塊棧內存又分配給其餘函數中的局部變量使用。多線程

打個比方:將棧區比做餐廳,局部變量比做客人,局部變量對應的棧內存比做餐具。客人吃飯時使用的餐具,在客人離開後由餐廳負責回收、清洗,而後再給其餘客人使用。同理,局部變量與棧內存的關係也是如此,當定義局部變量時,系統會在棧區爲其分配一塊內存空間,當函數執行結束後系統負責回收這塊內存,又分配給其餘局部變量使用。函數

#include<stdio.h>
void showA() //定義函數 showA
{
    int a;
    printf("&a=%p\n",&a); //輸出變量 a 的地址
}
void showB() //定義函數 showB
{
    int b;
    printf("&b=%p\n",&b); //輸出變量 b 的地址
}
int main(void)
{
    showA(); //調用 showA 函數
    showB(); //調用 showB 函數
    getchar();
    return 0;
}

運行結果如圖所示:操作系統

能夠驗證局部變量對應的內存在函數執行結束後,會被系統回收分配給其餘函數中的局部變量使用。線程

2. 棧內存注意事項

因爲局部變量在函數執行結束後,會被系統「自動回收」並分配給其餘函數中的局部變量使用。所以,在 C 程序中,不能將局部變量地址做爲函數返回值,不然會出現一些意想不到的效果。設計

下面經過例子來深刻了解一下。指針

#include<stdio.h>
int* showA()
{
    int a=1;
    return &a; //返回變量 a 的地址
}

void showB()
{
    int b=200;
}
int main(void)
{
    int* p_a=showA();
    printf("%d ",*p_a); //輸出 p_a 指向的變量的值
    showB(); //調用 showB 函數
    printf("%d ",*p_a); //輸出 p_a 指向的變量的值
    getchar();
    return 0;
}

// 運行結果
// 1 200

之因此會出現這種狀況,是因爲 showA 函數執行結束後,局部變量 a 對應的棧內存被系統回收後分配給 showB 函數中的局部變量 b 使用。所以,變量 a 和變量 b 對應同一塊棧內存,而指針變量 p_a 始終指向這塊棧內存。能夠認爲開始時 p_a 指向變量 a,調用 showB函數後,p_a 指向變量 b,因此第 16 行*p_a 的值爲 200。code

3. 堆內存

使用 malloc 系列函數分配的內存都屬於堆區,使用完後調用 free 函數進行釋放,不然可能會形成內存泄漏。blog

打個比方:堆區至關於本身家,malloc 分配的堆內存至關於盤子。在家裏吃飯時使用的盤子,吃完後必須手動進行清洗,不然盤子將不能再使用。同理,堆內存也是如此,使用完畢後,須要調用 free 函數進行手動釋放,不然這塊堆內存將沒法再次被使用。

malloc 函數
函數原型:
void *malloc(int size);
頭文件:
#include 
參數列表:
size:分配多少個字節。
功能:
申請指定大小的堆內存。
返回值:
若是分配成功則返回指向被分配內存的指針,不然返回空指針 NULL。

【說明】
void*表示「不肯定指向類型」的指針,使用前必須進行強制類型轉化,將 void*轉化爲「肯定指向類型」的指針。
必定要注意 malloc 的參數是「字節」,由於 malloc 不知道你申請的內存要放什麼類型的數據,因此統一的「匯率」就是「字節」。

free 函數
函數原型:
void free(void* ptr);
頭文件:
#include <stdlib.h>
參數列表:
ptr:指向要被釋放的堆內存。
功能:
釋放 ptr 指向的內存空間。

下面經過例子來了解如何在堆區分配內存。

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    int *p_int=(int*)malloc(sizeof(int));
    *p_int=200;
    printf("%p %d",p_int,*p_int);
    free(p_int);
    getchar();
    return 0;
}

若是內存申請可是忘了釋放(free),那麼就會致使「內存泄露」(memory leak)
運行結果如圖所示:

4. 堆內存注意事項

在 C 程序中,被 free 以後的堆內存,將會被操做系統回收分配給,不建議繼續使用,不然輸出的結果將難以預料。
下面經過例子來了解使用被 free 的堆內存。

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    int*p_int = (int*)malloc(sizeof(int));
    *p_int = 10;
    printf("%p %d\n",p_int,*p_int);
    free(p_int);
    printf("%p %d\n",p_int,*p_int);
    getchar();
    return 0;
}

運行結果如圖所示:

能夠看到 p_int 指向的堆內存地址沒有改變,可是該內存空間中的數據被修改了。這是由於被 free 的堆內存會被系統回收,分配給其餘地方使用,修改了這塊堆內存中的數據。

再也不使用的內存必定要及時的 free,不然會形成內存泄漏;還有用的內存也不能提早free。

5. 棧內存與堆內存分配限制

棧內存:
(1) 由系統自動分配、釋放。如:函數形參、局部變量。
(2) 棧內存比較小,在 VS2012 中,棧內存默認最大爲 1M,若是局部變量佔用的棧內存過大,會發生棧溢出。

下面經過例子來了解一下棧溢出。

#include<stdio.h>
int main(void)
{
    int a[9900000];
    getchar();
    return 0;
}

運行結果如圖所示:

定義 int 類型數組 a 長度爲 100 0000 已經超過了 1M,發生棧溢出錯誤。其中「Stackoverflow」中文意思就是棧溢出。

堆內存:
(1)由程序員本身申請、釋放。若是沒有釋放,可能會發生內存泄露,直到程序結束後由系統釋放。
(2)堆內存比較大,能夠分配超過 1G 的內存空間。

下面使用 malloc 分配大內存空間。

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    int *p_int=(int*)malloc(100000000);
    *p_int=100;
    printf("%d\n",*p_int);
    free(p_int);
    getchar();
    return 0;
}

運行後發現 malloc(100000000)分配成功,沒有報錯。

6. 函數內部返回數據的三種方式

方式一
在被調函數中使用 malloc 分配內存,在主調函數中 free 釋放內存。
例如:

#include<stdio.h>
#include<stdlib.h>
int* getMemory()
{
    int*p_int=(int*)malloc(sizeof(int));//被調函數分配內存
    *p_int=100;
    return p_int;
}
int main(void)
{
    int* p=getMemory();
    printf("%d\n",*p);
    free(p); //主調函數釋放內存
    getchar();
    return 0;
}

方式 1 分配內存與釋放內存是分開的,容易致使程序員忘記在主調函數中釋放內存,從而致使內存泄漏,所以方式 1 不推薦使用。

方式二
使用 static 修飾的局部變量,例如:

#include<stdio.h>
int* getMemory()
{
    static int a=100;
    return &a;
}
int main(void)
{
    int* p=getMemory();
    printf("%d ",*p);
    getchar();
    return 0;
}

方式 2 不適用於多線程等複雜的環境,所以也不推薦使用。

方式三
在主調函數中分配堆內存,在被調函數中使用堆內存,最後又在主調函數中釋放堆內存。
例如:

#include<stdio.h>
#include<stdlib.h>
void fun(int *p_int)
{
    *p_int=100;
}
int main(void)
{
    int* p=(int*)malloc(sizeof(int)); //主調函數分配堆內存
    fun(p);
    printf("%d",*p);
    free(p); //主調函數釋放堆內存
    getchar();
    return 0;
}

這是推薦的作法!

7. 初始化內存

使用 malloc 函數分配的堆內存,系統不會初始化內存,內存中殘留的仍是舊數據。所以,引用未初始化的堆內存,輸出的數據也將是未知的。例如:

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    int* p_int=(int*)malloc(sizeof(int));
    printf("%d",*p_int);
    getchar();
    return 0;
}

運行結果如圖所示:

輸出 p_int 指向堆內存中數據,因爲這塊內存未初始化,所以輸出結果將是難以預料的。爲了不引用堆內存中的未知數據,通常使用 malloc 在堆區分配內存後,須要將這塊堆內存初始化爲 0,例如:

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    int* p_int=(int*)malloc(sizeof(int));
    *p_int=0;
    printf("%d",*p_int);
    getchar();
    return 0;
}

上述這種初始化堆內存的方式,只適合於單個基本類型大小的堆內存。若是分配多個基本類型大小的堆內存時,這種賦值方式就不合適了。
例如:
int* p_int=(int*)malloc(sizeof(int)*10);
上述程序,分配了 10 個 int 類型字節大小的堆內存,若是仍採用賦值表達式進行初始化,就須要 for 循環初始化 10 次,太麻煩,因此 C 語言中提供了 memset 函數方便對內存進行初始化。

8. memset

函數原型:
void* memset(void* dest,int value,int size);
頭文件:
#include<string.h>
參數列表:
dest:被初始化的目標內存區域。
value:初始值。
size:初始化 size 個字節。
功能:
將 dest 指向的內存空間前 size 個字節初始化爲 value。
返回值:
返回 dest 指向的內存地址。

因爲是把 value 按照字節來進行填充的,而 value 是 int 類型(4 個字節),通常 value不要填充除了 0 以外的值,除非你很瞭解內存結構和二進制。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
    int* p_int=(int*)malloc(sizeof(int)*10);//注意不要寫成 malloc(10)
    int i=0;
    memset(p_int,0,sizeof(int)*10); //初始化堆內存
    for (i=0;i<10;i++)
    {
        printf("%d ",p_int[i]);
    }
    free(p_int);
    getchar();
    return 0;
}

(二) 結構體

在 C 語言中,char、int、float……等屬於系統內置的基本數據類型,每每只能解決簡單的問題。當遇到比較複雜的問題時,只使用基本數據類型是難以知足實際開發需求的。所以,C 語言容許用戶根據實際項目需求,自定義一些數據類型,而且用它們來定義變量。

1. 結構體概述

在前面介紹了 C 語言中的多種數據類型,例如:整型、字符型、浮點型、數組、指針……等等。可是在實際開發中,只有這些數據類型是不夠的,難以勝任複雜的程序設計。

例如:在員工信息管理系統中,員工的信息就是一類複雜的數據。每條記錄中都包括員工的姓名、性別、年齡、工號、工資等信息。姓名爲字符數組、性別爲字符、年齡爲整型、工號爲整型、工資爲整型。

對於這類數據,顯然不能使用數組存儲,由於數組各個元素的類型都是相同的。爲了解決這個問題,C 語言中提供了一種組合數據類型「結構體」。

結構體是一種組合數據類型,由用戶本身定義。結構體類型中的元素既能夠是基本數據類型,也能夠結構體類型。

定義結構體類型的通常格式爲:

struct 結構體名
{
    成員列表
};

成員列表由多個成員組成,每一個成員都必須做類型聲明,成員聲明格式爲:
數據類型 成員名;

下面來看一個具體的例子:

struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};

這段代碼中 struct 是關鍵字,Employee 結構體名,struct Employee 表示一種結構體類型。
該結構體中有 4 個成員,分別爲 name、age、id、salary,使用這種結構體類型就能夠表示員工的基本信息。

2. 定義結構體變量

在 C 語言中,定義結構體變量的方式有 3 種:
第 1 種 先定義結構體類型,再定義結構體變量,通常形式爲:

struct 結構體名
{
    成員列表
};
struct 結構體名 變量名;

例如:
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};
struct Employee emp;

這種方式和基本類型的變量定義方式相同,其中 struct Employee 是結構體類型名,emp是結構體變量名。

第 2 種 在定義結構體類型的同時定義變量,通常形式爲:

struct 結構體名
{
    成員列表
}變量名;
例如:
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
}emp,emp1,emp2;

這種方式將結構體類型定義與變量定義放在一塊兒,能夠直接看到結構體的內部結構,比較直觀。

第 3 種 直接定義結構體變量,而且不需指定結構體名,通常形式爲:

struct
{
    成員列表
}變量名;
例如:
struct
{
    char name[8];
    int age;
    int id;
    int salary;
}emp;

這種方式因爲沒有指定結構體名,顯然不能再使用該結構體類型去定義其餘變量,在實際開發中不多用到。

3. 初始化、引用結構體變量

(1)結構體變量初始化

在 C 語言中,結構體變量初始化,本質上是對結構體變量中的成員進行初始化,使用花括號{ }在初始化列表中對結構體變量中各個成員進行初始化,例如:

struct Employee emp={「hello」,20,1,10000}
或
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
}emp={「hello」,20,1,10000};

編譯器會將「hello」、20、一、10000按照順序依次賦值給結構體變量emp中的成員name、age、id、salary。

(2)引用結構體變量

引用結構體變量的本質,是引用結構體變量中的不一樣類型的成員,引用的通常形式爲:結構體變量名.成員名;

例如:emp.name 表示引用 emp 變量中的 name 成員,emp.id 表示引用 emp 變量中的id 成員。

其中「.」是成員運算符,它的優先級在全部運算符中是最高的。

#include<stdio.h>
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};
int main(void)
{
    struct Employee emp={"hello",20,1,10000};
    printf("%s\n",emp.name);
    printf("%d\n",emp.age);
    printf("%d\n",emp.id);
    printf("%d\n",emp.salary);
    getchar();
    return 0;
}

除了採用初始化列表,還可使用賦值運算符,對成員進行初始化,例如:

#include<stdio.h>
#include<string.h>
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};
int main(void)
{
    struct Employee emp;
    strcpy(emp.name,"hello");
    emp.age=20;
    emp.id=1;
    emp.salary=10000;
    printf("%s\n",emp.name);
    printf("%d\n",emp.age);
    printf("%d\n",emp.id);
    printf("%d\n",emp.salary);
    getchar();
    return 0;
}

【說明】

使用成員列表的方式初始化時,編譯器會自動將字符串「hello」複製到字符數組 name中。而使用成員賦值方式初始化時,須要調用 strcpy 函數,將字符串「hello」複製到字符數組 name 中。

4. 結構體指針

指向結構體變量的指針就是結構體指針,若是指針變量中保存一個結構體變量的地址,則這個指針變量指向該結構體變量,須要注意的是指針變量的類型必須和結構體變量的類型相同。

定義結構體指針變量的通常形式爲:
struct 結構體名 *指針變量名

例如:

struct Employee emp;
struct Employee * p_emp=&emp;

其中 emp 爲結構體變量,p_emp 爲結構體指針,將 emp 取地址賦給指針變量 p_emp表示 p_emp 指向 emp。
在 C 語言中,經過結構體指針 p 也能夠引用結構體中的成員,有如下兩種方式:
(1)(*p).成員名;
(2)p->成員名;

例如:struct Employee * p_emp=&emp;
(*p_emp)表示指向的結構體變量 emp,(*p_emp).age 表示指向的結構體變量 emp 中的成員 age。注意,「.」運算符優先級是最高的,(*p_emp)兩側的括號不能省略。

爲了簡化操做,C 語言容許將(*p).成員名用 p->成員名替換,(*p_emp).age 等價於p_emp->age,「->」稱爲指向運算符。

方式 1:

#include<stdio.h>
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};
int main(void)
{
    struct Employee emp={"hello",20,1,10000};
    struct Employee *p_emp=&emp;
    printf("%s\n", (*p_emp).name);
    printf("%d\n", (*p_emp).age);
    printf("%d\n", (*p_emp).id);
    printf("%d\n", (*p_emp).salary);
    getchar();
    return 0;
}

以「->」方式訪問結構體成員(經常使用)

#include<stdio.h>
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};
int main(void)
{
    struct Employee emp={"hello",20,1,10000};
    struct Employee *p_emp=&emp;
    printf("%s\n", p_emp->name);
    printf("%d\n", p_emp->age);
    printf("%d\n", p_emp->id);
    printf("%d\n", p_emp->salary);
    getchar();
    return 0;
}

到底用「.」仍是「->」初學者容易迷糊,記住一點:結構體變量用「.」,結構體指針變量用「->」

5. typedef 類型別名

在 C 語言中,除了使用 C 語言提供的標準類型名:char、int、double……以及自定義的結構體類型。還可使用 typedef 關鍵字指定一個新的類型名來代替已有的類型名,至關於給已有類型起別名。相似於現實生活中,給一我的起外號同樣,其實都是一我的。

typedef 的通常使用形式爲:
typedef 原類型名 新類型名

例如:
typedef int integer
其中 integer 是 int 類型的別名,在程序中可使用 integer 代替 int 來定義整型變量。

例如:
integer a,b;
等價於
int a,b;

下面經過例子來了解 typedef 的應用。

#include<stdio.h>
typedef int integer;
int main(void)
{
    integer a=10;
    printf("%d",a);
    getchar();
    return 0;
}

typedef 不只能夠爲基本類型起別名,還能夠爲自定義數據類型起別名,例如:

struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};
typedef struct Employee t_Employee;

其中 struct Employee 爲自定義結構體類型名,t_Employee 爲 struct Employee 的別名。在程序中可使用 t_Employee 替換 struct Employee。

下面經過例子來了解 typedef 在結構體中的應用。

#include<stdio.h>
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};
typedef struct Employee t_Employee; //定義別名
int main(void)
{
    t_Employee emp={"hello",20,1,10000};
    printf("%s\n",emp.name);
    printf("%d\n",emp.age);
    printf("%d\n",emp.id);
    printf("%d\n",emp.salary);
    getchar();
    return 0;
}

6. 結構體複製

在 C 語言中,容許相同類型的結構體變量之間相互賦值。
例如:
t_Employee emp={"hello",20,1,10000};
t_Employee emp2=emp;

執行 emp2=emp,會將結構體變量 emp 各個成員的值原樣複製一份到變量 emp2 各個成員中。和基本類型變量的賦值規則相同,emp2 是 emp 的一個拷貝。這種賦值方式,被稱爲「結構體複製」。

下面經過例子來了解結構體複製。

#include<stdio.h>
struct Employee
{
    char name[8];
    int age;
    int id;
    int salary;
};
typedef struct Employee t_Employee;
int main(void)
{
    t_Employee emp={"hello",20,1,10000};
    t_Employee emp2;
    emp2=emp;
    printf("%s\n",emp2.name);
    printf("%d\n",emp2.age);
    printf("%d\n",emp2.id);
    printf("%d\n",emp2.salary);
    getchar();
    return 0;
}
相關文章
相關標籤/搜索