目录
1. 程序内存
1.1 程序运行时的内存
1.2 内存开辟的方式
2. 动态内存函数
2.1 malloc
2.2 free
2.3 calloc
2.4 realloc
3. 常见的动态内存错误
3.1 对NULL指针的解引用操作
3.2 对动态开辟空间的越界访问
3.3 对非动态开辟内存使用free释放
3.4 使用free释放一块动态开辟内存的一部分
3.5 对同一块动态内存多次释放
3.6 动态开辟内存忘记释放(内存泄漏)
4. 通讯录
在学习C语言时,我们经常需要为一个变量来分配一块内存空间,不同类型的变量所占用的内存空间也是不一样的,而每一个程序内部都有各种代码和数据需要存储在内存上。因此,了解内存空间的分配对我们理解程序的运行是极为有利的,下图介绍了Linux x86-64运行时的内存映像
对于一个C程序,操作系统将会针对不同的数据类型在不同的内存区域上开辟空间
静态变量和全局变量被存储在数据段 ,在程序的整个运行周期都不销毁。其中,如果变量未初始化,则存放在.bss段,初始化的变量存放在.data段
#include
int a = 10; //全局变量(初始化)-.bss
int main()
{
static int a; //static修饰的静态变量(未初始化)-.data
return 0;
}
常量存放在代码段中的.rodata段,对于存放在这一块的常量是不可以被修改的,其状态为只读(read only data),但是注意并不是所有的常量都是放在常量数据段的,其特殊情况如下:
#include
int main()
{
printf("%s", "test"); //这里的字符串test就是一个常量字符串,存放在.rodata中
return 0;
}
可执行的代码和部分的只读常量存放在代码段的.text段,它与rodata段的主要不同是,text段是可以执行的,而且不被不同的进程共享
在内存中有一个部分称为栈区(stack) ,用来保存临时变量以及函数参数,栈区变量的生存周期是在入栈前(函数开始被调用)获得内存空间,而在出栈时(函数结束调用)释放内存空间。值得注意的是,栈的增长是由高地址向地址值增长的,并由栈指针%rsp来维护栈顶的地址
int main()
{
int a = 10; //临时变量,存放在栈区
test(a); //函数参数,存放在栈区
return 0;
}
在内存中有一个区域是专门服务于程序的,当程序需要动态地申请内存时,操作系统就会在这个部分开辟空间供程序使用——堆(heap)是最自由的一种内存,它完全由程序来负责内存的管理,包括什么时候申请,什么时候释放,而且对它的使用也没有什么大小的限制。在C/C++中,用malloc函数和new申请的内存都存在于heap段中
int main()
{
int* p = (int*)malloc(sizeof(int) * 4); //用malloc函数申请的4个int类型大小的空间-在堆区
return 0;
}
在C语言中,一般有两种开辟内存的方式
1. 静态开辟内存
int main()
{
int a = 10;
float b = 5.0;
char c[5] = { 0 };
return 0;
}
类似于这种语句,由于具有给定的数据类型,因此能够直接在内存中分配相应大小的空间来存储数据。比如int a = 10,就在内存中的栈区开辟了4个字节用于存放数据10;char c[5] = {0},在栈区开辟了10个字节的空间。这些空间的开辟有两个特点:
但这种内存开辟是有一定局限性的,考虑下面一种情况
程序需要不断更新数据,例如使用一个能存放10个整型的数组,在后续需要存入更多的数据,这时无法再去修改源程序,而如果开辟数组时将其长度指定得过大,又会造成内存的浪费,因此我们需要根据实际情况,动态的增长内存空间
2. 动态开辟内存
使用内存分配函数能够实现动态地开辟空间,下面将会介绍动态内存函数的使用
首先要介绍的第一个函数是malloc,函数定义为
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针
malloc的参数是一个无符号的size,这个size由程序来指定,其意义是在堆区分配空间的大小,如果size为10,操作系统会为程序在堆区分配一个大小为10字节的空间,并将使用权交给程序
malloc的返回值是一个无类型的指针,它指向分配好的空间的起始地址,通常在使用时需要根据数据类型进行强制转化
int main()
{
int* p = (int*)malloc(sizeof(int) * 4);
if(NULL != p)
{
//使用
}
return 0;
}
这个代码说明
注意事项
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数定义为
void free (void* ptr);
free函数是用来回收动态分配的内存空间的 ,参数ptr是指向动态分配空间首地址的指针
一般来说,在程序结束后,操作系统会自动释放在之前为程序动态开辟的内存空间,但如果程序出错或,会导致操作系统无法将内存空间收回,造成内存泄漏 ,因此,当内存分配的空间不再使用时,程序员使用free函数主动释放动态分配的空间是一种很好的编码习惯
int main()
{
int* p = (int*)malloc(sizeof(int) * 4);
free(p); //释放掉动态申请的空间p
p = NULL;
return 0;
}
注意事项
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。函数定义为
void* calloc (size_t num, size_t size);
如果需要对开辟的空间进行初始化,通常使用calloc函数
realloc函数能够在malloc和colloc函数动态开辟的空间的基础上,接着开辟空间,函数定义为
void* realloc (void* ptr, size_t size);
参数ptr是之前就动态开辟好的内存空间的起始地址,参数size是接着要开辟空间的大小。返回值是改变后的空间的起始地址,下面会详细讲述
考虑下面的情况
int main()
{
int* p = (int*)calloc(4, sizeof(int));
if (p != NULL)
{
p = (int*)realloc(p, sizeof(int) * 5);
}
return 0;
}
上述代码先动态分配16个字节的空间,之后使用realloc在p后面分配20个字节的空间,因此这一块连续空间的大小为36个字节
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);
}
void test()
{
int a = 10;
int* p = &a;
free(p); //报错
}
void test()
{
int* p = (int*)malloc(100);
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();
while (1);
}
使用内存的动态分配,实现动态版本的通讯录,代码如下:
contact.h
#pragma once
#include
#include
#include
#include
//类型的声明
#define MAX 1000
#define NAME_MAX 20
#define SEX_MAX 5
#define TELE_MAX 12
#define ADDR_MAX 30
#define DEFAULT_SZ 3
//定义一个结构体, 包含联系人信息
typedef struct PeoInfo
{
char name[NAME_MAX];
char sex[SEX_MAX];
int age;
char tele[TELE_MAX];
char addr[ADDR_MAX];
}PeoInfo;
//定义一个结构体, 作为通讯录(静态的数组版本)
//typedef struct Contact
//{
// PeoInfo* data[MAX]; //可以存放1000个人的信息
// int sz; //记录通讯录中已经保存的信息个数
//}Contact;
//动态版本
typedef struct Contact
{
PeoInfo* data; //指向动态分配的空间
int sz; //记录通讯录中已经保存的信息个数
int capacity; //记录通讯录当前的最大容量
}Contact;
//函数的声明
//初始化通讯录
void InitContact(Contact* pc);
//销毁通讯录
void DestoryContact(Contact* pc);
//增容的函数实现
void CheckCapacity(Contact* pc);
//增加联系人的信息
void AddContact(Contact* pc);
//打印通讯录中的信息
void PrintContact(const Contact* pc);
//删除指定联系人
void DelContact(Contact* pc);
//查找指定联系人
void SearchContact(const Contact* pc);
//修改指定联系人的信息
void ModifyContact(Contact* pc);
//排序联系人信息
void SortConcatc(Contact* pc);
//清空通讯录
void EmptyContact(Contact* pc);
contact.c
#include "contact.h"
//初始化通讯录的函数实现
void InitContact(Contact* pc)
{
assert(pc);
pc->sz = 0;
pc->capacity = DEFAULT_SZ;
pc->data = (PeoInfo*)malloc(pc->capacity * sizeof(PeoInfo));
//如果malloc失败
if (pc->data == NULL)
{
perror("InitContact::malloc");
return;
}
memset(pc->data, 0, pc->capacity * sizeof(PeoInfo));
}
//销毁通讯录的函数实现
void DestoryContact(Contact* pc)
{
free(pc->data);
pc->data = NULL;
pc->sz = 0;
pc->capacity = 0;
printf("销毁成功\n");
}
//增容的函数实现
void CheckCapacity(Contact* pc)
{
if (pc->sz == pc->capacity)
{
PeoInfo* tmp = (PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo)); //每次增容两个
if (tmp != NULL)
{
pc->data = tmp;
}
else
{
perror("CheckCapacity::realloc");
}
pc->capacity += 2;
printf("增容成功\n");
}
}
//增加联系人的函数实现
void AddContact(Contact* pc)
{
assert(pc);
CheckCapacity(pc); //每次要添加联系人, 先检查容量是否已满
//录入信息
printf("请输入名字:>");
scanf("%s", pc->data[pc->sz].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pc->sz].age));
printf("请输入性别:>");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入电话:>");
scanf("%s", pc->data[pc->sz].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pc->sz].addr);
pc->sz++;
printf("添加成功\n");
}
//打印通讯录的函数实现
void PrintContact(const Contact* pc)
{
assert(pc);
int i = 0;
printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
for (int i = 0;i < pc->sz;i++)
{
printf("%-20s %-5d %-5s %-12s %-30s\n", pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
}
}
//通讯录中查找的函数实现
int FindByName(const Contact* pc, char name[])
{
assert(pc);
int i = 0;
for (i = 0;i < pc->sz;i++)
{
if (0 == strcmp(pc->data[i].name, name))
{
return i;
}
}
return -1;
}
//删除通讯录信息的函数实现
void DelContact(Contact* pc)
{
assert(pc);
//无有效信息
if (pc->sz == 0)
{
printf("通讯录已空, 无法删除\n");
return;
}
//删除联系人的函数实现
char name[NAME_MAX] = { 0 };
printf("请输入要删除人的名字:>");
scanf("%s", name);
int pos = FindByName(pc, name);
//1.无效名字
if (pos == -1)
{
printf("要删除的人不存在\n");
return;
}
//2.删除
int j = 0;
for (j = pos;j < pc->sz - 1;j++)
{
pc->data[j] = pc->data[j + 1];
}
pc->sz--;
printf("删除成功\n");
}
//查找联系人的函数实现
void SearchContact(const Contact* pc)
{
assert(pc);
char name[NAME_MAX] = { 0 };
printf("请输入要查找人的名字:>");
scanf("%s", name);
int pos = FindByName(pc, name);
if (pos == -1)
{
printf("要查找的人不存在\n");
return;
}
//找到了, 打印pos位置上联系人的信息
printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
printf("%-20s %-5d %-5s %-12s %-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[NAME_MAX] = { 0 };
printf("请输入要修改信息人的名字:>");
scanf("%s", name);
int pos = FindByName(pc, name);
if (pos == -1)
{
printf("要修改信息人不存在\n");
return;
}
//修改pos位置上联系人的信息
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");
}
//排序联系人信息的函数实现
void SortConcatc(Contact* pc)
{
assert(pc);
//按照姓氏的首字母排序
for (int i = 0;i < pc->sz - 1;i++)
{
for (int j = 0;j < pc->sz - 1 - i;j++)
{
if (strcmp(pc->data[j].name, pc->data[j + 1].name) > 0)
{
PeoInfo tmp = pc->data[j];
pc->data[j] = pc->data[j + 1];
pc->data[j + 1] = tmp;
}
}
}
printf("排序完成\n");
}
//清空通讯录的函数实现
void EmptyContact(Contact* pc)
{
assert(pc);
pc->sz = 0;
printf("清空成功\n");
}
test.c
#include "contact.h"
enum Option
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SORT,
PRINT,
EMPTY
};
void menu()
{
printf("*************************\n");
printf("*** 1.add 2.del ***\n");
printf("*** 3.search 4.modify ***\n");
printf("*** 5.sort 6.print ***\n");
printf("*** 7.empty 0.exit ***\n");
printf("*************************\n");
}
void test()
{
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 SORT:
SortConcatc(&con);
break;
case PRINT:
PrintContact(&con);
break;
case EMPTY:
EmptyContact(&con);
break;
case EXIT:
DestoryContact(&con);
printf("退出通讯录\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}