
想要節省空間,你必須要知道——動態記憶體管理 (附通訊錄動態記憶體版原始碼)
- 1. 為什么存在動態記憶體分配
- 2. 動態記憶體函式的介紹
- 2.1 malloc
- 2.2 free
- malloc和free通常配合一起使用:
- 2.3 calloc
- 2.4 realloc
- 3. 常見的動態記憶體錯誤
- 4. 幾個經典的筆試題
- 題目1:
- 代碼分析:
- 代碼改正:
- 題目2:
- 代碼分析:
- 代碼改正:
- 題目3 :
- 代碼分析:
- 代碼改正:
- 題目4 :
- 代碼分析:
- 代碼改正:
- 5. 柔性陣列
- 通訊錄(動態儲存版本)原始碼
(附通訊錄動態記憶體版原始碼))
1. 為什么存在動態記憶體分配
我們已經掌握的記憶體開辟方式有:
int val = 20;//在堆疊空間上開辟四個位元組
char arr[10] = {0};//在堆疊空間上開辟10個位元組的連續空間
但是上述的開辟空間的方式有兩個特點:
1 . 空間開辟大小是固定的,
2 . 陣列在申明的時候,必須指定陣列的長度,它所需要的記憶體在編譯時分配,
但是對于空間的需求,不僅僅是上述的情況,
有時候我們需要的空間大小在程式運行的時候才能知道,那陣列的編譯時開辟空間的方式就不能滿足了,
這時候就只能試試動態存開辟了,
2. 動態記憶體函式的介紹
2.1 malloc
C語言為我們提供了一個動態記憶體開辟的函式:
描述
C 庫函式
void *malloc(size_t size)分配所需的記憶體空間,并回傳一個指向它的指標,
宣告
void *malloc(size_t size)
引數
size– 記憶體塊的大小,以位元組為單位,
回傳值
該函式回傳一個
指標,指向已分配大小的記憶體,如果請求失敗,則回傳NULL,
注意點:
1.如果開辟成功,則回傳一個指向開辟好空間的指標,
2.如果開辟失敗,則回傳一個NULL指標,因此malloc的回傳值一定要做檢查,
3.回傳值的型別是void*,所以malloc函式并不知道開辟空間的型別,具體在使用的時候使用者自己來決定,
4.如果引數size 為 0,malloc的行為是標準是未定義的,取決于編譯器,
5.malloc開辟的記憶體空間是在堆上的,不會自動釋放空間,
2.2 free
由于
malloc是在堆空間上開辟記憶體,不會被自動釋放,容易造成記憶體泄漏
這時候,C語言里提供了一個free函式,來人為釋放動態記憶體開辟的空間,將空間還給作業系統
描述
C 庫函式 void free(void *ptr) 釋放之前呼叫
calloc、malloc 或 realloc 所分配的記憶體空間,
宣告
void free(void *ptr)
引數
ptr – 指標指向一個要釋放記憶體的記憶體塊,該記憶體塊
之前是通過呼叫 malloc、calloc 或 realloc 進行分配記憶體的,如果傳遞的引數是一個空指標,則不會執行任何動作,
回傳值
該函式
不回傳任何值,
注意:
1.如果引數 ptr 指向的空間不是動態開辟的,那free函式的行為是
未定義的,
2.如果引數 ptr 是NULL指標,則函式什么事都不做,
3.通常在free完之后,要ptr=NULL;將指標給置空,否則當釋放了空間,這塊空間的指標仍然存在,就會造成一個野指標
malloc和free通常配合一起使用:
舉個栗子
#include <stdio.h>
int main()
{
//代碼1
int num = 0;
scanf("%d", &num);
int arr[num] = {0};
//代碼2
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判斷ptr指標是否為空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr);//釋放ptr所指向的動態記憶體
ptr = NULL;//是否有必要?答案是很有必要
return 0;
}
2.3 calloc
描述
C 庫函式
void *calloc(size_t nitems, size_t size) 分配所需的記憶體空間,并回傳一個指向它的指標,malloc和calloc之間的不同點是,malloc不會設定記憶體為零,而calloc會設定分配的記憶體為零,
宣告
void *calloc(size_t nitems, size_t size)
引數
nitems– 要被分配的元素個數,
size– 元素的大小,
回傳值
該函式回傳一個指標,指向已分配的記憶體,如果請求失敗,則回傳
NULL,
注意:
1.函式的功能是為 num 個大小為 size 的元素開辟一塊空間,并且把空間的每個位元組初始化為0,
2.與函式 malloc 的區別只在于 calloc 會在回傳地址之前把申請的空間的每個位元組初始化為全0,
舉個例子:
malloc不會初始化空間,cd就是隨機值的意思

calloc會初始化空間為0

2.4 realloc
描述
C 庫函式
void *realloc(void *ptr, size_t size)嘗試重新調整之前呼叫malloc 或 calloc所分配的ptr所指向的記憶體塊的大小,
宣告
void *realloc(void *ptr, size_t size)
引數
ptr– 指標指向一個要重新分配記憶體的記憶體塊,該記憶體塊之前是通過呼叫 malloc、calloc 或 realloc 進行分配記憶體的,如果為空指標,則會分配一個新的記憶體塊,且函式回傳一個指向它的指標,
size– 記憶體塊的新的大小,以位元組為單位,如果大小為 0,且 ptr 指向一個已存在的記憶體塊,則 ptr 所指向的記憶體塊會被釋放,并回傳一個空指標,
回傳值
該函式回傳一個指標 ,指向重新分配大小的記憶體,如果請求失敗,則
回傳 NULL,
注意:
這個函式調整原記憶體空間大小的基礎上,還會將原來記憶體中的資料移動到
新的空間,
realloc在調整記憶體空間的時候存在兩種情況:
情況1:原有空間之后沒有足夠大的空間
情況2:原有空間之后有足夠大的空間
因為有兩種情況的存在,所以我們在使用realloc函式的同時要注意檢查回傳的是否為空指標
#include <stdio.h>
int main()
{
int *ptr = malloc(100);
if(ptr != NULL)
{
//業務處理
}
else
{
exit(EXIT_FAILURE);
}
//擴展容量
//代碼1
ptr = realloc(ptr, 1000);//這樣可以嗎?(如果申請失敗會如何?)
// 答案是不可以,有可能會追加開辟記憶體失敗,然后丟失原有記憶體
//代碼2
int*p = NULL;
p = realloc(ptr, 1000);//通過一個中間變數來判斷是否追加開辟記憶體成功
if(p != NULL)
{
ptr = p;
}
//業務處理
free(ptr);
return 0; }
3. 常見的動態記憶體錯誤
對NULL指標的解參考操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就會有問題
free(p);
}
對動態開辟空間的越界訪問
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//當i是10的時候越界訪問
}
free(p);
}
對非動態開辟記憶體使用free釋放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
//這樣不可以,會報錯,非堆上的動態記憶體不能用free來釋放
}
使用free釋放一塊動態開辟記憶體的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向動態記憶體的起始位置,程式會掛掉
//free釋放的是p指向的空間,p必須指向所要釋放空間的起始地址
}
對同一塊動態記憶體多次釋放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重復釋放,會報錯
}
動態開辟記憶體忘記釋放(記憶體泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
//這里應該free掉開辟的動態記憶體空間
while(1);
}
忘記釋放不再使用的動態開辟的空間會造成記憶體泄漏,
切記: 動態開辟的空間一定要釋放,并且正確釋放 ,
4. 幾個經典的筆試題
題目1:
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
運行Test會有什么結果?
答案是會程式會掛掉
代碼分析:
錯誤原因;
①str傳給p的時候,是值傳遞,p是str的臨時拷貝,所以當malloc開辟的空間起始地址放在p中時,不會影響str,str依然為NULL
②當str時NULL,strcpy想把hello world拷貝到str指向的空間時,程式就崩潰了,因為NULL指標指向的空間是不能直接訪問的
圖解:*

代碼改正:

題目2:
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
運行Test會有什么結果?
答案是
代碼分析:
錯誤原因;
①p是區域變數(區域變數是存在堆疊區的),函式呼叫完之后就會隨著函式空間的銷毀而銷毀,將記憶體空間還給作業系統
②回傳的p實際上已經是一個野指標了,指向的是未知的空間
圖解:

代碼改正:

題目3 :
void GetMemory(char **p, int num) {
*p = (char *)malloc(num);
}
void Test(void) {
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
運行Test會有什么結果?
答案是
記憶體泄漏!!!!
代碼分析:
錯誤原因;
①malloc申請了記憶體空間,是在堆區上的,是不會自動銷毀的
②如果在使用完成之后沒有free掉這塊空間,會造成記憶體泄漏,記憶體泄漏是指程式中已動態分配的的堆記憶體,由于某些原因無法釋放或者未釋放,造成的記憶體浪費,
圖解:

代碼改正:

題目4 :
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
運行Test會有什么結果?
答案是
資料非法訪問
代碼分析:
錯誤原因;
①free完之后沒有將指標置空,造成了野指標的存在
②野指標會導致非法訪問行為
圖解:

代碼改正:

5. 柔性陣列
也許你從來沒有聽說過柔性陣列(flexible array)這個概念,但是它確實是存在的,
C99 中,結構中的最后一個元素允許是未知大小的陣列,這就叫做『柔性陣列』成員,
例如:
typedef struct st_type
{
int i;
int a[0];//柔性陣列成員
}type_a;
有些編譯器會報錯無法編譯可以改成:
typedef struct st_type
{
int i;
int a[];//柔性陣列成員
}type_a;
柔性陣列的特點:
- 結構中的柔性陣列成員前面必須至少一個其他成員,
- sizeof 回傳的這種結構大小不包括柔性陣列的記憶體,
- 包含柔性陣列成員的結構用malloc ()函式進行記憶體的動態分配,并且分配的記憶體應該大于結構的大小,以適應柔性陣列的預期大小,
例如:
//code1
typedef struct st_type
{
int i;
int a[0];//柔性陣列成員
}type_a;
printf("%d\n", sizeof(type_a));//輸出的是4
柔性陣列的使用
//代碼1
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//業務處理
p->i = 100;
for(i=0; i<100; i++) {
p->a[i] = i; }
free(p);
這樣柔性陣列成員a,相當于獲得了100個整型元素的連續空間,
柔性陣列的優勢
上述的 type_a 結構也可以設計為:
//代碼2
typedef struct st_type
{
int i;
int *p_a; }type_a;
type_a *p = malloc(sizeof(type_a));
p->i = 100; p->p_a = (int *)malloc(p->i*sizeof(int));
//業務處理
for(i=0; i<100; i++) {
p->p_a[i] = i; }
//釋放空間
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
上述 代碼1 和 代碼2 可以完成同樣的功能
但是 方法1 的實作有兩個好處:
第一個好處是:方便記憶體釋放
如果我們的代碼是在一個給別人用的函式中,你在里面做了二次記憶體分配,并把整個結構體回傳給用戶,用戶呼叫free可以釋放結構體,但是用戶并不知道這個結構體內的成員也需要free,所以你不能指望用戶來發現這個事,所以,如果我們把結構體的記憶體以及其成員要的記憶體一次性分配好了,并回傳給用戶一個結構體指標,用戶做一次free就可以把所有的記憶體也給釋放掉,
第二個好處是:這樣有利于訪問速度.
連續的記憶體有益于提高訪問速度,也有益于減少記憶體碎片,(其實,我個人覺得也沒多高了,反正你跑不了要用做偏移量的加法來尋址)
通訊錄(動態儲存版本)原始碼
通訊錄.c
#include "contact.h"
void menu()
{
printf("******************************\n");
printf("**** 1. 添加 2. 洗掉 **\n");
printf("**** 3. 搜索 4. 修改 **\n");
printf("**** 5. 展示全部 6. 排序 **\n");
printf("**** 0. 退出 **\n");
printf("******************************\n");
}
int main()
{
int input = 0;
//創建一個通訊錄
struct Contact con;
//初始化通訊錄
InitContact(&con);
do
{
menu();
printf("請選擇:>");
scanf_s("%d", &input);
switch (input)
{
case ADD:
AddContact(&con);
break;
case DEL:
DeletContact(&con);
break;
case SHOW:
ShowContact(&con);
break;
case MODIFY:
ModifyContact(&con);
break;
case SEARCH:
SearchContact(&con);
break;
case SORT:
SortContact(&con);
break;
case EXIT:
//銷毀通訊錄
DestroyContact(&con);
printf("退出通訊錄\n");
break;
default:
printf("選擇錯誤\n");
break;
}
} while (input);
return 0;
}
contact.c
#include "contact.h"
//靜態初始化
//void InitContact(struct Contact* pc)
//{
// pc->sz = 0;//默認沒有資訊
// memset(pc->data, 0, MAX*sizeof(struct PeoInfo));
// memset(pc->data, 0, sizeof(pc->data));
//}
//動態初始化
void InitContact(struct Contact* pc)
{
pc->sz = 0;
pc->data = (struct PeoInfo*)malloc(DEFAULT_SZ * sizeof(struct PeoInfo));
pc->capacity = DEFAULT_SZ;//初始最大容量為3
}
//靜態添加
//void AddContact(struct Contact* pc)
//{
// if (pc->sz == MAX)
// {
// printf("通訊錄滿了\n");
// }
// else
// {
// printf("請輸入名字:>");
// scanf_s("%s", pc->data[pc->sz].name, 30);
// printf("請輸入年齡:>");
// scanf_s("%d", &(pc->data[pc->sz].age));
// printf("請輸入性別:>");
// scanf_s("%s", pc->data[pc->sz].sex, 5);
// printf("請輸入電話:>");
// scanf_s("%s", pc->data[pc->sz].tele, 12);
// printf("請輸入地址:>");
// scanf_s("%s", pc->data[pc->sz].addr, 30);
//
//
// printf("添加成功\n");
// pc->sz++;
// ShowContact(pc);
// }
//}
//動態添加
void AddContact(struct Contact* pc)
{
if (pc->sz == pc->capacity)
{
struct PeoInfo* ptr = (struct PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(struct PeoInfo));
if (ptr != NULL)
{
pc->data = ptr;
pc->capacity += 2;
printf("增容成功\n");
}
else
{
return;
}
printf("增容成功\n");
}
//錄入新增人的資訊
printf("請輸入名字:>");
scanf_s("%s", pc->data[pc->sz].name, 30);
printf("請輸入年齡:>");
scanf_s("%d", &(pc->data[pc->sz].age));
printf("請輸入性別:>");
scanf_s("%s", pc->data[pc->sz].sex, 5);
printf("請輸入電話:>");
scanf_s("%s", pc->data[pc->sz].tele, 12);
printf("請輸入地址:>");
scanf_s("%s", pc->data[pc->sz].addr, 30);
printf("添加成功\n");
pc->sz++;
ShowContact(pc);
}
void DeletContact(struct Contact* pc)
{
printf("請輸入需要洗掉的聯系人姓名\n");
char name[30] = "0";
scanf_s("%s", name, 30);
for (int i = 0; i < pc->sz; i++)
{
if (strcmp(name, pc->data[i].name) == 0)
{
for (int j = i; j < pc->sz-1; j++)
{
strcpy_s(pc->data[j].name, 30, pc->data[j + 1].name);
strcpy_s(pc->data[j].sex, 5, pc->data[j + 1].sex);
strcpy_s(pc->data[j].tele, 12, pc->data[j + 1].tele);
strcpy_s(pc->data[j].addr, 30, pc->data[j + 1].addr);
pc->data[j].age = pc->data[j + 1].age;
}
printf("洗掉成功\n");
(pc->sz)--;
ShowContact(pc);
}
}
}
void ModifyContact(struct Contact* pc)
{
printf("請輸入需要修改的聯系人姓名\n");
char name[30] = "0";
scanf_s("%s", name, 30);
for (int i = 0; i < pc->sz; i++)
{
if (strcmp(name, pc->data[i].name) == 0)
{
printf("請輸入名字:>");
scanf_s("%s", pc->data[i].name, 30);
printf("請輸入年齡:>");
scanf_s("%d", &(pc->data[i].age));
printf("請輸入性別:>");
scanf_s("%s", pc->data[i].sex, 5);
printf("請輸入電話:>");
scanf_s("%s", pc->data[i].tele, 12);
printf("請輸入地址:>");
scanf_s("%s", pc->data[i].addr, 30);
printf("修改成功!\n");
ShowContact(pc);
}
}
}
void ShowContact(struct Contact* pc)
{
int i = 0;
printf("序號\t%10s\t%10s\t%8s\t%15s\t%30s\n", "name", "age", "sex", "tele", "addr");
for (i = 0; i < pc->sz ; i++)
{
//列印每一個資料
printf("%d\t%10s\t%10d\t%8s\t%15s\t%30s\n",
i + 1,
pc->data[i].name,
pc->data[i].age,
pc->data[i].sex,
pc->data[i].tele,
pc->data[i].addr);
}
}
void SearchContact(struct Contact* pc)
{
printf("請輸入需要搜索的聯系人姓名\n");
char name[30] = "0";
scanf_s("%s", name, 30);
for (int i = 0; i < pc->sz; i++)
{
if (strcmp(name, pc->data[i].name) == 0)
{
printf("序號\t%10s\t%10s\t%8s\t%15s\t%30s\n", "name", "age", "sex", "tele", "addr");
printf("%d\t%10s\t%10d\t%8s\t%15s\t%30s\n",
i + 1,
pc->data[i].name,
pc->data[i].age,
pc->data[i].sex,
pc->data[i].tele,
pc->data[i].addr);
return;
}
}
printf("找不到聯系人資訊\n");
}
void SortContact(struct Contact* pc)
{
struct PeoInfo temp;
for (int j = 0; j < pc->sz - 1; j++)
for (int i = 0; i < pc->sz - 1 - j; i++)
{
if (strcmp(pc->data[i].name, pc->data[i + 1].name) > 0)
{
temp = pc->data[i + 1];
pc->data[i + 1] = pc->data[i];
pc->data[i] = temp;
}
}
ShowContact(pc);
}
void DestroyContact(struct Contact* pc)
{
free(pc->data);
pc->data = NULL;
pc->capacity = 0;
pc->sz = 0;
}
contact.h
#pragma once
#define NAME_MAX 30
#define SEX_MAX 5
#define TELE_MAX 12
#define ADDR_MAX 30
#define MAX 1000
#define DEFAULT_SZ 3 //默認大小為3
#include <string.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//創建列舉變數
enum Option
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT
};
//描述人的資訊
struct PeoInfo
{
char name[NAME_MAX];
int age;
char sex[SEX_MAX];
char tele[TELE_MAX];
char addr[ADDR_MAX];
};
//通訊錄-靜態版本
//struct Contact
//{
// struct PeoInfo data[MAX];//1000個人的資料存放在data陣列中
// int sz;//記錄當前通訊錄有效資訊的個數
//};
//動態增長的版本
struct Contact
{
struct PeoInfo* data;
int sz;//通訊錄中當前有效元素的個數
int capacity;//通訊錄的當前最大容量
};
//初始化通訊錄
void InitContact(struct Contact* pc);
//增加聯系人
void AddContact(struct Contact* pc);
//洗掉聯系人
void DeletContact(struct Contact* pc);
//修改聯系人資訊
void ModifyContact(struct Contact* pc);
//搜索聯系人資訊
void SearchContact(struct Contact* pc);
//顯示所有的聯系人
void ShowContact(struct Contact* pc);
//按姓氏排序聯系人資訊
void SortContact(struct Contact* pc);
//銷毀通訊錄
void DestroyContact(struct Contact* pc);
本文為學習中的知識總結,如有錯誤請評論區留言,感激不盡~
原創不易,如果覺得還不錯,留個三連不過分吧~ 有訪必回~
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/304372.html
標籤:其他
上一篇:【演算法學習】1689. 十-二進制數的最少數目(java / c / c++ / python / go / rust)
下一篇:線下精準大資料獲客



