第八章 內存管理
8.1 作用域
C語言變量的作用域分爲:
- 代碼塊作用域(代碼塊是{}之間的一段代碼)
- 函數作用域
- 文件作用域
8.1.1 局部變量
局部變量也叫auto
自動變量(auto
可寫可不寫),一般情況下代碼塊{}內部定義的變量都是自動變量,它有如下特點:
- 在一個函數內定義,只在函數範圍內有效
- 在複合語句中定義,只在複合語句中有效
- 隨着函數調用的結束或複合語句的結束局部變量的聲明聲明週期也結束
- 如果沒有賦初值,內容爲隨機
#include <stdio.h>
void test()
{
//auto寫不寫是一樣的
//auto只能出現在{}內部
auto int b = 10;
}
int main(void)
{
//b = 100; //err, 在main作用域中沒有b
if (1)
{
//在複合語句中定義,只在複合語句中有效
int a = 10;
printf("a = %d\n", a);
}
//a = 10; //err離開if()的複合語句,a已經不存在
return 0;
}
8.1.2 靜態(static)局部變量
static
局部變量的作用域也是在定義的函數內有效static
局部變量的生命週期和程序運行週期一樣,同時staitc
局部變量的值只初始化一次,但可以賦值多次static
局部變量若未賦以初值,則由系統自動賦值:數值型變量自動賦初值0,字符型變量賦空字符
#include <stdio.h>
void fun1()
{
int i = 0;
i++;
printf("i = %d\n", i);
}
void fun2()
{
//靜態局部變量,沒有賦值,系統賦值爲0,而且只會初始化一次
static int a;
a++;
printf("a = %d\n", a);
}
int main(void)
{
fun1();
fun1();
fun2();
fun2();
return 0;
}
8.1.3 全局變量
- 在函數外定義,可被本文件及其它文件中的函數所共用,若其它文件中的函數調用此變量,須用extern聲明
- 全局變量的生命週期和程序運行週期一樣
- 不同文件的全局變量不可重名
8.1.4 靜態(static)全局變量
- 在函數外定義,作用範圍被限制在所定義的文件中
- 不同文件靜態全局變量可以重名,但作用域不衝突
static
全局變量的生命週期和程序運行週期一樣,同時staitc
全局變量的值只初始化一次
8.1.5 extern全局變量聲明
extern int a;
聲明一個變量,這個變量在別的文件中已經定義了,這裏只是聲明,而不是定義。
8.1.6 全局函數和靜態函數
在C語言中函數默認都是全局的,使用關鍵字static可以將函數聲明爲靜態,函數定義爲static就意味着這個函數只能在定義這個函數的文件中使用,在其他文件中不能調用,即使在其他文件中聲明這個函數都沒用。
對於不同文件中的staitc函數名字可以相同。
注意:
- 允許在不同的函數中使用相同的變量名,它們代表不同的對象,分配不同的單元,互不干擾。
- 同一源文件中,允許全局變量和局部變量同名,在局部變量的作用域內,全局變量不起作用。
- 所有的函數默認都是全局的,意味着所有的函數都不能重名,但如果是
staitc
函數,那麼作用域是文件級的,所以不同的文件static
函數名是可以相同的。
8.1.7 總結
類型 | 作用域 | 生命週期 |
---|---|---|
auto變量 | 一對{}內 | 當前函數 |
static局部變量 | 一對{}內 | 整個程序運行期 |
extern變量 | 整個程序 | 整個程序運行期 |
static全局變量 | 當前文件 | 整個程序運行期 |
extern函數 | 整個程序 | 整個程序運行期 |
static函數 | 當前文件 | 整個程序運行期 |
register變量 | 一對{}內 | 當前函數 |
8.2 內存佈局
8.2.1 內存分區
C代碼經過預處理、編譯、彙編、鏈接4步後生成一個可執行程序。
在 Linux 下,程序是一個普通的可執行文件,以下列出一個二進制可執行文件的基本情況:
通過上圖可以得知,在沒有運行程序前,也就是說程序沒有加載到內存前,可執行程序內部已經分好3段信息,分別爲代碼區(text)、數據區(data)和未初始化數據區(bss)3 個部分(有些人直接把data和bss合起來叫做靜態區或全局區)。
-
代碼區
存放 CPU 執行的機器指令。通常代碼區是可共享的(即另外的執行程序可以調用它),使其可共享的目的是對於頻繁被執行的程序,只需要在內存中有一份代碼即可。代碼區通常是隻讀的,使其只讀的原因是防止程序意外地修改了它的指令。另外,代碼區還規劃了局部變量的相關信息。 -
全局初始化數據區/靜態數據區(data段)
該區包含了在程序中明確被初始化的全局變量、已經初始化的靜態變量(包括全局靜態變量和局部靜態變量)和常量數據(如字符串常量)。 -
未初始化數據區(又叫 bss 區)
存入的是全局未初始化變量和未初始化靜態變量。未初始化數據區的數據在程序開始執行之前被內核初始化爲 0 或者空(NULL)。
程序在加載到內存前,代碼區和全局區(data和bss)的大小就是固定的,程序運行期間不能改變。然後,運行可執行程序,系統把程序加載到內存,除了根據可執行程序的信息分出代碼區(text)、數據區(data)和未初始化數據區(bss)之外,還額外增加了棧區、堆區。
-
代碼區(text segment)
加載的是可執行文件代碼段,所有的可執行代碼都加載到代碼區,這塊內存是不可以在運行期間修改的。 -
未初始化數據區(BSS)
加載的是可執行文件BSS段,位置可以分開亦可以緊靠數據段,存儲於數據段的數據(全局未初始化,靜態未初始化數據)的生存週期爲整個程序運行過程。 -
全局初始化數據區/靜態數據區(data segment)
加載的是可執行文件數據段,存儲於數據段(全局初始化,靜態初始化數據,文字常量(只讀))的數據的生存週期爲整個程序運行過程。 -
棧區(stack)
棧是一種先進後出的內存結構,由編譯器自動分配釋放,存放函數的參數值、返回值、局部變量等。在程序運行過程中實時加載和釋放,因此,局部變量的生存週期爲申請到釋放該段棧空間。 -
堆區(heap)
堆是一個大容器,它的容量要遠遠大於棧,但沒有棧那樣先進後出的順序。用於動態內存分配。堆在內存中位於BSS區和棧區之間。一般由程序員分配和釋放,若程序員不釋放,程序結束時由操作系統回收。
8.2.2 存儲類型總結
類型 | 作用域 | 生命週期 | 存儲位置 |
---|---|---|---|
auto變量 | 一對{}內 | 當前函數 | 棧區 |
static局部變量 | 一對{}內 | 整個程序運行期 | 初始化在data段,未初始化在BSS段 |
extern變量 | 整個程序 | 整個程序運行期 | 初始化在data段,未初始化在BSS段 |
static全局變量 | 當前文件 | 整個程序運行期 | 初始化在data段,未初始化在BSS段 |
extern函數 | 整個程序 | 整個程序運行期 | 代碼區 |
static函數 | 當前文件 | 整個程序運行期 | 代碼區 |
register變量 | 一對{}內 | 當前函數 | 運行時存儲在CPU寄存器 |
字符串常量 | 當前文件 | 整個程序運行期 | data段 |
#include <stdio.h>
#include <stdlib.h>
int e;
static int f;
int g = 10;
static int h = 10;
int main()
{
int a;
int b = 10;
static int c;
static int d = 10;
char *i = "test";
char *k = NULL;
printf("&a\t %p\t //局部未初始化變量\n", &a);
printf("&b\t %p\t //局部初始化變量\n", &b);
printf("&c\t %p\t //靜態局部未初始化變量\n", &c);
printf("&d\t %p\t //靜態局部初始化變量\n", &d);
printf("&e\t %p\t //全局未初始化變量\n", &e);
printf("&f\t %p\t //全局靜態未初始化變量\n", &f);
printf("&g\t %p\t //全局初始化變量\n", &g);
printf("&h\t %p\t //全局靜態初始化變量\n", &h);
printf("i\t %p\t //只讀數據(文字常量區)\n", i);
k = (char *)malloc(10);
printf("k\t %p\t //動態分配的內存\n", k);
return 0;
}
8.2.3 存儲類型總結內存操作函數
1.memset()
#include <string.h>
void *memset(void *s, int c, size_t n);
功能:將s的內存區域的前n個字節以參數c填入
參數:
s:需要操作內存s的首地址
c:填充的字符,c雖然參數爲int,但必須是unsigned char , 範圍爲0~255
n:指定需要設置的大小
返回值:s的首地址
int a[10];
memset(a, 0, sizeof(a));
memset(a, 97, sizeof(a));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%c\n", a[i]);
}
2.memcpy()
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
功能:拷貝src所指的內存內容的前n個字節到dest所值的內存地址上。
參數:
dest:目的內存首地址
src:源內存首地址,注意:dest和src所指的內存空間不可重疊
n:需要拷貝的字節數
返回值:dest的首地址
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int b[10];
memcpy(b, a, sizeof(a));
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d, ", b[i]);
}
printf("\n");
//memcpy(&a[3], a, 5 * sizeof(int)); //err, 內存重疊
3.memmove()
memmove()
功能用法和memcpy()
一樣,區別在於:dest
和src
所指的內存空間重疊時,memmove()
仍然能處理,不過執行效率比memcpy()
低些。
4.memcmp()
#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
功能:比較s1和s2所指向內存區域的前n個字節
參數:
s1:內存首地址1
s2:內存首地址2
n:需比較的前n個字節
返回值:
相等:=0
大於:>0
小於:<0
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int b[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int flag = memcmp(a, b, sizeof(a));
printf("flag = %d\n", flag);
8.2.4 堆區內存分配和釋放
1.malloc()
#include <stdlib.h>
void *malloc(size_t size);
功能:在內存的動態存儲區(堆區)中分配一塊長度爲size字節的連續區域,用來存放類型說明符指定的類型。分配的內存空間內容不確定,一般使用memset初始化。
參數:
size:需要分配內存大小(單位:字節)
返回值:
成功:分配空間的起始地址
失敗:NULL
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
int count, *array, n;
printf("請輸入要申請數組的個數:\n");
scanf("%d", &n);
array = (int *)malloc(n * sizeof (int));
if (array == NULL)
{
printf("申請空間失敗!\n");
return -1;
}
//將申請到空間清0
memset(array, 0, sizeof(int)*n);
for (count = 0; count < n; count++) /*給數組賦值*/
array[count] = count;
for (count = 0; count < n; count++) /*打印數組元素*/
printf("%2d", array[count]);
free(array);
return 0;
}
2.free()
#include <stdlib.h>
void free(void *ptr);
功能:釋放ptr所指向的一塊內存空間,ptr是一個任意類型的指針變量,指向被釋放區域的首地址。對同一內存空間多次釋放會出錯。
參數:
ptr:需要釋放空間的首地址,被釋放區應是由malloc函數所分配的區域。
返回值:無
8.3 內存分區代碼分析(在Linux下測試)
8.3.1 返回棧區地址
#include <stdio.h>
int a = 10;
int *fun()
{
return &a;//函數調用完畢,a釋放
}
int main(int argc, char *argv[])
{
int *p = NULL;
p = fun();
*p = 100; //操作野指針指向的內存,err
return 0;
}
8.3.2 返回data區地址
#include <stdio.h>
int *fun()
{
static int a = 10;
return &a; //函數調用完畢,a不釋放
}
int main(int argc, char *argv[])
{
int *p = NULL;
p = fun();
*p = 100; //ok
printf("*p = %d\n", *p);
return 0;
}
8.3.3 值傳遞1
#include <stdio.h>
#include <stdlib.h>
void fun(int *tmp)
{
tmp = (int *)malloc(sizeof(int));
*tmp = 100;
}
int main(int argc, char *argv[])
{
int *p = NULL;
fun(p); //值傳遞,形參修改不會影響實參
printf("*p = %d\n", *p);//err,操作空指針指向的內存
return 0;
}
8.3.4 值傳遞2
#include <stdio.h>
#include <stdlib.h>
void fun(int *tmp)
{
*tmp = 100;
}
int main(int argc, char *argv[])
{
int *p = NULL;
p = (int *)malloc(sizeof(int));
fun(p); //值傳遞
printf("*p = %d\n", *p); //ok,*p爲100
return 0;
}
8.3.5 返回堆區地址
#include <stdio.h>
#include <stdlib.h>
int *fun()
{
int *tmp = NULL;
tmp = (int *)malloc(sizeof(int));
*tmp = 100;
return tmp;//返回堆區地址,函數調用完畢,不釋放
}
int main(int argc, char *argv[])
{
int *p = NULL;
p = fun();
printf("*p = %d\n", *p);//ok
//堆區空間,使用完畢,手動釋放
if (p != NULL)
{
free(p);
p = NULL;
}
return 0;
}