简单不先于复杂,而是在复杂之后
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟4个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1.空间开辟大小是固定的。
2.数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
这个时候就需要动态内存开辟了。
C语言提供了一个动态内存开辟的函数;
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查。
返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
如果参数 size 为 0 ,malloc 的行为是标准未定义的,取决于编译器。
C语言提供了另外一个函数 free ,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
free 函数用来释放动态内存开辟的内存。
malloc 和 free 都声明在 stdlib.h 头文件中。
#include
#include
int main()
{
int num = 0;
scanf("%d", &num);
//int arr[num] = { 0 };
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;
}
C语言还提供了一个函数叫 calloc, calloc 函数也用来动态内存分配。
void* calloc (size_t num, size_t size);
#include
#include
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (NULL != p)
{
//使用空间
}
free(p);
p = NULL;
return 0;
}
所以如果我们对申请的内存空间的内容要求初始化,可以用 calloc 函数来完成任务。
void* realloc (void* ptr, size_t size);
情况1
当是情况1的时候,要扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2
当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
由于上述两种情况,realloc 函数的使用就要注意一些。
#include
#include
int main()
{
int* ptr = (int*)malloc(100);
if (ptr != NULL)
{
//业务处理
}
else
{
exit(EXIT_FAILURE);
}
//扩展容量
//
ptr = (int*)realloc(ptr, 1000);
int* p = NULL;
p = realloc(ptr, 1000);
if (p != NULL)
{
ptr = p;
}
//业务处理
free(ptr);
return 0;
}
动态版本的通讯录:
- 通讯录默认能存放3个人的信息
- 如果空间不够了,就增加空间,每次增加2个人的空间
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"
enum Option
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT
};
void menu()
{
printf("**********************************************\n");
printf("*********** 1. add 2. del. *******\n");
printf("*********** 3. search 4. modify *******\n");
printf("*********** 5. show 6. sort *******\n");
printf("*********** 0. exit *******\n");
printf("**********************************************\n");
}
int main()
{
int input = 0;
Contact con;//通讯录
//初始化通讯录
InitContact(&con);
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case ADD:
AddContact(&con);
break;
case DEL:
DelContact(&con);
break;
case SEARCH:
SearchContact(&con);
break;
case MODIFY:
ModifyContact(&con);
break;
case SHOW:
ShowContact(&con);
break;
case SORT:
SortContact(&con);
break;
case EXIT:
DestroyContact(&con);
printf("退出通讯录\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
contact.h
#pragma once
#include
#include
#include
#include
#define DEFAULT_SZ 3
#define INC_SZ 2
#define MAX 100
#define MAX_NAME 20
#define MAX_SEX 10
#define MAX_TELE 12
#define MAX_ADDR 30
//类型的声明
//人的信息
typedef struct PeoInfo
{
char name[MAX_NAME];
int age;
char sex[MAX_SEX];
char tele[MAX_TELE];
char addr[MAX_ADDR];
}PeoInfo;
通讯录
静态版本
//typedef struct Contact
//{
// PeoInfo data[MAX];//存放人的信息
// int count;//记录当前通讯录中实际人的个数
//
//}Contact;
//动态版本
typedef struct Contact
{
PeoInfo* data;//存放人的信息3
int count;//记录当前通讯录中实际人的个数
int capacity;//当前通讯录的容量
}Contact;
//初始化通讯录
int InitContact(Contact* pc);
//增加联系人到通讯录
void AddContact(Contact* pc);
//打印通讯录
void ShowContact(const Contact* pc);
//删除指定联系人
void DelContact(Contact* pc);
//查找指定联系人
void SearchContact(const Contact* pc);
//修改指定联系人
void ModifyContact(Contact* pc);
//排序通讯录中的内容
//按照名字来排序
//按照年龄来排序
//.....
void SortContact(Contact* pc);
//销毁通讯录
void DestroyContact(Contact* pc);
contact.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"
//静态版本
//void InitContact(Contact* pc)
//{
// assert(pc);
// pc->count = 0;
// memset(pc->data, 0, sizeof(pc->data));
//}
//动态版本
int InitContact(Contact* pc)
{
assert(pc);
pc->count = 0;
pc->data = (PeoInfo*)calloc(DEFAULT_SZ, sizeof(PeoInfo));
if (pc->data == NULL)
{
printf("InitContact:%s\n", strerror(errno));
return 1;
}
pc->capacity = DEFAULT_SZ;
}
静态版本
//void AddContact(Contact* pc)
//{
// assert(pc);
// if (pc->count == MAX)
// {
// printf("通讯录已满,无法添加\n");
// return;
// }
//
// printf("请输入名字:>");
// scanf("%s", pc->data[pc->count].name);
//
// printf("请输入年龄:>");
// scanf("%d", &(pc->data[pc->count].age));
// //其他成员是数组,数组名本身就是地址,age是整形变量,所以要取地址
//
// printf("请输入性别:>");
// scanf("%s", pc->data[pc->count].sex);
//
// printf("请输入电话:>");
// scanf("%s", pc->data[pc->count].tele);
//
// printf("请输入地址:>");
// scanf("%s", pc->data[pc->count].addr);
//
// pc->count++;
// printf("增加成功\n");
//}
//
void CheckCapacity(Contact* pc)
{
if (pc->count == pc->capacity)
{
PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + INC_SZ) * sizeof(PeoInfo));
if (ptr == NULL)
{
printf("AddContact:%s\n", strerror(errno));
return;
}
else
{
pc->data = ptr;
pc->capacity += INC_SZ;
//增容成功
}
}
}
//动态版本
void AddContact(Contact* pc)
{
assert(pc);
//增容
CheckCapacity(pc);
printf("请输入名字:>");
scanf("%s", pc->data[pc->count].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pc->count].age));
//其他成员是数组,数组名本身就是地址,age是整形变量,所以要取地址
printf("请输入性别:>");
scanf("%s", pc->data[pc->count].sex);
printf("请输入电话:>");
scanf("%s", pc->data[pc->count].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pc->count].addr);
pc->count++;
printf("增加成功\n");
}
void ShowContact(const Contact* pc)
{
assert(pc);
int i = 0;
printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n", "名字", "年龄", "性别", "电话", "地址", "");
for (i = 0; i < pc->count; i++)
{
printf("%-20s\t%-3d\t%-5s\t%-12s\t%-30s\n",
pc->data[i].name,
pc->data[i].age,
pc->data[i].sex,
pc->data[i].tele,
pc->data[i].addr);
}
}
void DelContact(Contact* pc)
{
char name[MAX_NAME];
assert(pc);
if (pc->count == 0)
{
printf("通讯录为空,没有信息可以删除\n");
return;
}
printf("请输入要删除人的名字:>\n");
scanf("%s", name);
//1.查找
int pos = FindByName(pc, name);
if (pos == -1)
{
printf("要删除的人不存在\n");
return;
}
//2.删除
int i = 0;
for (i = pos; i < pc->count-1; i++)
{
pc->data[i] = pc->data[i + 1];
}
pc->count--;
printf("删除成功\n");
}
static int FindByName(Contact* pc, char name[])
{
assert(pc);
int i = 0;
for (i = 0; i < pc->count; i++)
{
if (0 == strcmp(pc->data[i].name, name))
{
return i;
}
}
return -1;
}
void SearchContact(const Contact* pc)
{
assert(pc);
char name[MAX_NAME];
printf("请输入要查找人的名字:>\n");
scanf("%s", name);
//1.查找
int pos = FindByName(pc, name);
if (pos == -1)
printf("要查找的人不存在\n");
else
printf("%-20s\t%-3d\t%-5s\t%-12s\t%-30s\n",
pc->data[pos].name,
pc->data[pos].age,
pc->data[pos].sex,
pc->data[pos].tele,
pc->data[pos].addr);
}
void ModifyContact(Contact* pc)
{
assert(pc);
char name[MAX_NAME];
printf("请输入要修改人的名字:>\n");
scanf("%s", name);
//1.查找
int pos = FindByName(pc, name);
if (pos == -1)
{
printf("要修改的人不存在\n");
}
else
{
printf("要修改人的信息已经查找到,接下来开始修改\n");
printf("请输入名字:>");
scanf("%s", pc->data[pos].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pos].age));
printf("请输入性别:>");
scanf("%s", pc->data[pos].sex);
printf("请输入电话:>");
scanf("%s", pc->data[pos].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pos].addr);
printf("修改成功\n");
}
}
int cmp_peo_bt_name(const void* e1, const void* e2)
{
return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name);
}
//按照名字来排序
void SortContact(Contact* pc)
{
assert(pc);
qsort(pc->data, pc->count, sizeof(PeoInfo), cmp_peo_bt_name);
printf("排序成功\n");
}
void DestroyContact(Contact* pc)
{
assert(pc);
free(pc->data);
pc->data = NULL;
}
int main()
{
int* p = (int*)malloc(40);
*p = 20;//如果p接收到的是空指针,就会出现问题
free(p);
p = NULL;
return 0;
}
解决方法:对 malloc、calloc、realloc这种动态内存开辟的函数返回值进行检测
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
*p = 20;
free(p);
p = NULL;
return 0;
}
int main()
{
int* p = (int*)malloc(40);
if (p = NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i <= 10; i++)
{
p[i] = i;
}
free(p);
p = NULL;
return 0;
}
int main()
{
int a = 10;
int* p = &a;
//......
free(p);
p = NULL;
return 0;
}
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
//使用
int i = 0;
for (i = 0; i < 5; i++)
{
*p = i;
p++;
}
//释放
free(p);
p = NULL;
return 0;
}
int main()
{
int* p = (int*)malloc(40);
//...
free(p);
//...
free(p);
return 0;
}
p 指向的动态开辟的空间已经释放了,但是 p 还是指向那一个空间,是十分危险的,p 此时是一个野指针,所以在 free 之后要把指针置空。
void test()
{
int* p = (int*)malloc(100);
//...
int flag = 0;
scanf("%d\n", &flag);
if (flag == 5)
{
return;
}
free(p);
p = NULL;
}
int main()
{
test();
return 0;
}
int* test()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
return p;
}
//...
return p;
}
int main()
{
int* ret = test();
//忘记释放了
return 0;
}
忘记释放不再使用的动态开辟的空间会造成内存泄露。
切记:动态开辟的内存一定要释放,并且正确释放
以下题目出自《高质量的C/C++编程》
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
运行test函数会有什么结果?
GetMemory 函数中的 p 是形参,是局部变量,出作用域被销毁,但是动态内存开辟的空间还在,发生内存泄漏。
strcpy 中拷贝字符串的目标地址仍是空指针,在解引用一个空指针的时候程序会崩溃。
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
//str 存放的就是动态内存开辟的100个字节的空间的起始地址
strcpy(str, "hello world");
printf(str);
//释放
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
这样修改,就解决了问题。
下面是第二种改法:
char* GetMemory()
{
char* p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
运行Test会出现什么结果?
在函数
GetMemory
中,创建了一个局部数组p
并将其地址返回。然而,一旦函数执行完毕,p
将被销毁,其内存将被释放。因此,返回的指针将指向无效的内存地址。
修复这个问题的一种方法是使用动态内存分配,例如
malloc
,并确保在不再需要时释放该内存。
int* test()
{
//返回栈空间的地址
//栈上的数据都是临时的
//函数执行完毕后,栈上的数据会被弹出,释放相应的空间。
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("hehe\n");
printf("%d\n", *p);
return 0;
}
这段代码存在一个潜在的问题,涉及到指针和生命周期的问题。
在
test
函数中,创建了一个整数a
并返回了它的地址。然而,一旦函数执行完毕,a
将超出其作用域,其内存将被释放。因此,返回的指针将指向一个无效的内存地址。
当尝试打印
*p
,但由于a
已经超出了作用域,p
指向的内存可能已经被其他数据覆盖,这将导致未定义的行为。如果不打印"hehe\n",内存不会被覆盖,打印出的还是10,若加上,打印出来的就是4
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函数会有什么结果?
会打印 hello ,但是忘记了free,造成内存泄漏。
在 Test 最后面加上
free(str); str = NULL;
就得到了解决。
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
运行Test会出现什么结果?
其中 str 指向的内存空间被释放后没有及时置空,str 成为了野指针,仍旧指向原来的地址,判定非空后将“world”字符串拷贝到原来地址后面的内存中,属于非法访问,要尽量避免这种情况。
所以,在动态开辟空间后要及时释放空间,并将指向空间起始位置的指针置空。
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
- 数据段(静态区)(static): 存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被 static 修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁。
所以生命周期变长。
柔性数组(flexible array)
C99 中,结构体中的最后一个元素允许是未知大小的数组,这就叫做[柔性数组]成员。
typedef struct S
{
int i;
int a[0];//柔性数组成员
}S;
有些编译器会报错无法编译还可以改成:
typedef struct S
{
int i;
int a[];//柔性数组成员
}S;
typedef struct S
{
int i;
int a[0];
}S;
int main()
{
printf("%d\n", sizeof(S));//输出4
return 0;
}
typedef struct S
{
int i;
int a[0];
}S;
int main()
{
int i = 0;
S* p = (S*)malloc(sizeof(S) + 100 * sizeof(int));
if(p == NULL)
{
return 1;
}
//业务处理
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p);
p = NULL;
return 0;
}
这样柔性数组成员 a ,相当于获得了 100 个整型元素的连续空间。
上述的代码也可以设计为:
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
return 1;
}
ps->n = 100;
ps->arr = (int*)malloc(40);
if (ps->arr == NULL)
{
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
//扩容
int* ptr = (int*)realloc(ps->arr, 80);
if (ptr == NULL)
{
return 1;
}
//使用
//释放
free(ps->arr);
free(ps);
ps = NULL;
return 0;
}
两个代码可以完成同样的功能,但是使用柔性数组的实现有两个好处:
**第一个好处是:**方便内存释放
如果我们的代码是在一个给别人用的一个函数中,我们在里面做了二次内存分配,并把整个结构体返回给用户。
用户调用 free 可以释放结构体,但是用户并不知道这个结构体的成员也需要 free ,所以不能指望用户来发现这件事。
所以,如果我们把结构体的内存以及成员要的内存一次性分配好了,并返回用户一个结构体指针,用户做一次 free 就可以把所有的内存也给释放掉。
**第二个好处是:**这样有利于访问速度。
连续的内存有益于提高访问速度,也有益于减少内存碎片。
有一篇文章很好:C语言结构体里的数组和指针
//业务处理
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p);
p = NULL;
return 0;
}
这样柔性数组成员 a ,相当于获得了 100 个整型元素的连续空间。
### 6.3 柔性数组的优势
上述的代码也可以设计为:
```c
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
return 1;
}
ps->n = 100;
ps->arr = (int*)malloc(40);
if (ps->arr == NULL)
{
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
//扩容
int* ptr = (int*)realloc(ps->arr, 80);
if (ptr == NULL)
{
return 1;
}
//使用
//释放
free(ps->arr);
free(ps);
ps = NULL;
return 0;
}
两个代码可以完成同样的功能,但是使用柔性数组的实现有两个好处:
**第一个好处是:**方便内存释放
如果我们的代码是在一个给别人用的一个函数中,我们在里面做了二次内存分配,并把整个结构体返回给用户。
用户调用 free 可以释放结构体,但是用户并不知道这个结构体的成员也需要 free ,所以不能指望用户来发现这件事。
所以,如果我们把结构体的内存以及成员要的内存一次性分配好了,并返回用户一个结构体指针,用户做一次 free 就可以把所有的内存也给释放掉。
**第二个好处是:**这样有利于访问速度。
连续的内存有益于提高访问速度,也有益于减少内存碎片。
有一篇文章很好:C语言结构体里的数组和指针