【C语言】动态内存管理基础知识——动态通讯录,如何实现通讯录容量的动态化

简介: 动态内存管理的函数有:malloc,calloc,ralloc,free,本文讲解动态内存函数和使用,如何进行动态内存管理,实现通讯录联系人容量的动态化,对常见动态内存错误进行总结。

为什么存在动态内存分配

int main()
{
    int arr[20] = {0};//整形数组开辟20个元素,就是80个字节。
    return 0;
}

1.空间开辟大小是固定的

2.数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

如果我们开辟的空间不够,那么进行修改会比较麻烦。如果开辟的空间较大,那么所占据的空间又会很大。那么有没有一种方法可以用多少空间就开辟多大的内存呢?

这个时候就有了动态内存开辟。

malloc

动态内存函数的头文件  <stdlib.h>

void* malloc (size_t size)

申请内存块,

size

就是我们要申请的字节大小。当然,我们的内存是有限的,不是想要申请多少内存就可以申请多少内存。

返回值

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。


开创的空间放回地址是void*类型,使用的时候记得要强制类型转换。

所以我们在使用malloc函数时,要检测返回值p是否是空指针。

#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include<stdio.h>
int main()
{
  int arr[10] = { 0 };
  //动态内存开辟
  int* p = (int*)malloc(40);
  if (p == NULL)      //检测返回的p是不是空指针。
  {
    printf("%s\n", strerror(errno));
    return 1;
  }
  //使用动态内存
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i;
  }
  for (i = 0; i < 10; i++)
  {
    printf("%d ", *(p + i));
  }
  //释放
  free(p);    //常常搭配malloc、calloc、realloc使用,后面会讲到
  p = NULL;
  return 0;
}


动态内存空间位置

动态内存函数是在堆区开辟内存空间的。我们一般的局部变量、形式参数都是存放在栈区。


内存泄漏问题

什么是内存泄漏?

我们创建的局部变量,数据会在函数结束时释放。

动态内存空间存放临时使用的数据,这些数据不必等到函数结束时释放,而是需要时随时开辟,不需要时,随时释放。动态开辟的内存使用完是要进行释放的,如果不对内存进行释放,那么开辟的动态内存就会被之前的数据占据,这部分的内存就无法使用,相当于丢失了内存。因此我们把这类问题叫做内存泄漏。


下面的代码如果吧进行释放,那么就会一直占据内存空间。



通过释放动态内存,内存可以被重新调用


最后我们加了一步,p = NULL,p的内存空间已经被释放了还给操作系统了,但是p还是原来的地址,我们通过p就会使用到一个已经释放的内存,这就会导致野指针问题。为了避免这种情况,我们将p的地址去掉就可以了。


free

void free (void* ptr)

free函数是专门用来释放动态开辟的内存。

  • 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数ptr 是NULL指针,则函数什么事都不做。

free函数只能释放动态内存开辟的空间,如果释放其他空间,就会报错。

#include <stdlib.h>
int main()
{
  int a = 0;
  int* p = &a;
  free(p);
  p = NULL;
  return 0;
}


calloc  

void* calloc (size_t num, size_t size)


num

开辟空间元素的个数

size

空间中每个元素的大小。

  • 函数的功能为num个大小为size 的元素开辟一块空间,并且把空间的每个字节初始化为0
  • callocmalloc的区别在于calloc在放回地址之前把申请的空间的每个字节初始化为全0。

下面的例子就是解释:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
  int* p = (int*)calloc(10, sizeof(int));
  if (p == NULL)
  {
    printf("%s\n", strerror(errno));
  }
  //打印
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    printf("%d ", *(p + i));
  }
  //释放
  free(p);
  p = NULL;
  return 0;
}

realloc

void* realloc (void* ptr, size_t size)


ptr

要调整的空间的起始位置。

size

  • realloc函数的出现让动态内存管理更加灵活。
  • 有时我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间豁达了,那为了合理的内存,我们一定对内存的大小做灵活的调整。那realloc函数可以做到对动态开辟内存大小的调整。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
  int* p = (int*)malloc(40);
  if (p == NULL)
  {
    printf("%s\n", strerror(errno));
  }
  //使用
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i + 1;
  }
  //扩容
  int* ptr =  realloc(p, 80);
  if (ptr != NULL)  //这里不直接将新的地址赋给p,是因为realloc有可能开辟失败返回空指针。后面详细讲
  {
    p = ptr;
  }
  for(i=0;i<10;i++)
  {
    printf("%d ", *(p + i));
  }
  //释放
  free(p);
  p = NULL;
  return 0;
}

realloc如何开辟动态内存空间

realloc在调整内存空间的是存在两种情况。

第一种情况,原空间之后有足够大的空间。当要进行调整的内存空间后面有多余的40个字节空间,那么就可以直接开辟向后面开辟40个字节的空间,然后放回起始位置的地址,这里指的是0的地址。



第二种情况原空间之后没有足够大的空间。要调整的内存空间后面不足以存放40个字节的空间,那么就要重新找到一个新的地址(可以存放80个字节)开辟,开辟成功后返回起始位置的地址。

注意:如果需要开辟的空间过大,是会开辟失败的,开辟失败,realloc返回空指针,所以要检查空指针。

这里把realloc开辟的空间换成8000,来实现第二种情况。




动态版本的通讯录

动态:

要实现通讯录容量的动态化,要实现两个功能

1.通讯录默认能存放3个人的信息

2.如果空间不够了,就增加空间,每次增加2个人的空间


静态:

原来的通讯录的信息由结构体组成的数组,来存放信息,设置的是100人的信息。

1.当没有100人的信息时,会造成空间上的浪费。

2.当超过100人的信息时,又无法自动扩容,如果想要扩容要手动改变最大存放的空间。

原通讯录的代码:

有兴趣可以学习通讯录的实现http://t.csdnimg.cn/UbT9I

存放数据的改变

首先把存放联系人信息的结构体给大家看看

typedef struct PeoInfo
{
  char name[MAX_NAME];
  int age;
  char sex[MAX_SEX];
  char tele[MAX_TELE];
  char addr[MAX_ADDR];
}PeoInfo;


原来存放联系人的信息是通过数组data[100],这样的数组的空间是固定的,是一开始设置的最大容量。

typedef struct Contact
{
  PeoInfo data[MAX];
  int count;//记录当前通讯录中实际人数的个数
}Contact;

不以数组的形式进行存放,以数组改成地址,但是地址也可以像数组一样访问数据。data是联系人的起始地址,如果想要访问第二个人,就是*(data+1),等于data[1]。


count是当前使用了多少个联系人的空间.


capacity记录的是现在存放联系人的容量是多大。因为当每次容量不够时,就增加空间,每次增加2个人的空间。也就是每次count == capacity的时候,capacity就要加2.

typedef struct Contact
{
  PeoInfo* data;
  int count;//记录当前通讯录中实际人数的个数
  int capacity;
}Contact;

1.初始化的改变

assert函数,用来检验空指针,如果为空指针,就会报错。

参数pc,是创立的struct Contact结构体变量的地址,这里是传址调用,作用就是改变原来的数据。

原通讯录是存放100个联系人的数组,将100个联系人的数据初始化为0。

void InitContact(Contact* pc)
{
  assert(pc);
  pc->count = 0;
  memset(pc->data, 0, sizeof(pc->data));
}

动态通讯录要实现开辟3个联系人的空间,并将它们进行初始化。 既要开辟空间,又要进行初始化,我们想到calloc函数。

malloc是单纯地开辟空间,realloc是既开辟空间,并进行初始化。malloc和realloc地区别就在于是否对开辟的空间初始化。

开辟3个联系人空间,calloc进行开辟,将地址传给pc->data.将记录联系人的容量传给capacity.

int InitContact(Contact* pc)
{
  assert(pc);
  pc->count = 0;
  pc->data =(int*) calloc(3, sizeof(PeoInfo));
  if (pc == NULL)
  {
    printf("InitContact::%s\n", strerror(errno));
    return 1;
  }
  pc->capacity = 3;
  return 0;
}

2.增加联系人

参数pc,是创立的struct Contact结构体变量的地址,这里是传址调用,作用就是改变原来的数据。

动态内存管理,通过pc->data[count]可以进行数据的输入。最重要的是实现通讯录容量的动态化。

count表示已经使用的联系人数量,capacity表示联系人的总容量。

当count == capacity时,就要动态开辟内存,对容量进行增容。

CheckCapacity为自定义增容函数,我们要实现增容的功能。

realloc重新开辟内存块,实现内存的动态化。realloc返回的起始地址不能直接传给data,因为动态内存的开辟有可能失败,失败传回空指针。

capacity增加2,最后提示增容成功。

void CheckCapacity(Contact* pc)
{
  if (pc->count == pc->capacity);
  {
    PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo));
    if (ptr == NULL)
    {
      printf("AddContact::%s\n", strerror(errno));
      return 1;
    }
    else
    {
      pc->data = ptr;
      pc->capacity += 2;
      printf("增容成功\n");
    }
  }
}
void AddContact(Contact* pc)
{
  assert(pc);
  //增容
  CheckCapacity(pc);
  printf("请输入名字:》");
  scanf("%s", pc->data[pc->count].name);
  printf("请输入年龄:》");
  scanf("%d", &(pc->data[pc->count].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");
}

将整体的代码呈现在文章末尾。

       

常见动态内存错误

       

1.对NULL空指针进行解引用

如果开辟的空间过大,malloc有可能开辟失败,开辟失败就会返回空指针。如果直接对p进行解引用,就会产生问题。

#include <stdlib.h>
int main()
{
  int* p = (int*)malloc(40);
  *p = 20;
  return 0;
}

正确解决方法:

在开辟动态内存后对p进行检验,是否为空指针。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
  int* p = (int*)malloc(40);
  if (p == NULL)
  {
        printf("%s\n", strerror(errno));
    return 1;
  }
  *p = 20;
  free(p);
  p = NULL;
  return 0;
}

2.对动态开辟空间的越界访问

我们只开辟了10个字节的空间,但是访问,从0到10,10算进去的话就是11个元素,这里访问越界了,就会出问题。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
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;
}

正确解决方法:

一定要注意我们开辟的空间是否和访问的空间是一样的。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
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;
}

3.对非动态开辟的内存使用free释放

free只能够释放动态开辟的内存,不能够随意去使用函数。

#include <stdlib.h>
int main()
{
  int a = 10;
  int* p = &a;
  //....
  free(p);
  p = NULL;
  return 0;
}

4.使用free释放动态开辟内存的一部分

free做不到释放动态内存的一部分,如果要释放要将整个动态内存进行释放。

#include <stdlib.h>
int main()
{
  int* p = (int*)malloc(40);
  if (p = NULL)
  {
    return 1;
  }
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    *p = i;
    p++;
  }
  free(p);
  p = NULL;
  return 0;
}

正确解决方法:

不改变p的位置,对p进行释放。使用p的话,通过:

*(p+i) = i;

5.对同一块动态内存多次释放

free的二次使用,第二次使用的动态内存空间已经还给操作系统了,但是还能对p进行操作,就是野指针问题。

#include <stdlib.h>
int main()
{
  int* p = (int*)malloc(40);
  free(p);
  //....
  free(p);
  return 0;
}

正确解决方法:

避免free的二次使用,或者将p转化为空指针。

#include <stdlib.h>
int main()
{
  int* p = (int*)malloc(40);
  free(p);
  p = NULL;
  free(p);
  return 0;
}

6.动态开辟内存忘记释放(内存释放)

看看下面的例子:

如果flag = 5的话,那么后面free函数就会跳过,动态开辟的内存就不能释放。

#include <stdio.h>
#include <stdlib.h>
void test()
{
  int* p = (int*)malloc(100);
  int flag = 0;
  scanf("%d", &flag);
  if (flag == 5)
  {
    return;
  }
  free(p);
  p = NULL;
}
int main()
{
  test();
  return 0;
}

动态通讯录的实现

contact.h

头文件:用来对函数的声明

#pragma once
#include <string.h>
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#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;
  int count;//记录当前通讯录中实际人数的个数
  int capacity;
}Contact;
//初始化通讯录
int InitContact(Contact* pc);
//销毁通讯录
void DestroyContact(Contact* pc);
//增加联系人到通讯录
void AddContact(Contact* pc);
//打印通讯录中信息
void ShowContact(const Contact* pc);
//删除联系人的信息
void DelContact(Contact* pc);
//查找指定联系人
void SeachContact(Contact* pc);
//修改指定联系人
void ModifyContact(Contact* pc);
//排序通讯录中的内容
//按照名字来排序
void SortContact(Contact* pc);

contact.c

源文件:函数的实现和定义

#define _CRT_SECURE_NO_WARNINGS
#include "contact.h"
//动态版本
int InitContact(Contact* pc)
{
  assert(pc);
  pc->count = 0;
  pc->data =(int*) calloc(DEFAULT_SZ, sizeof(PeoInfo));
  if (pc == NULL)
  {
    printf("InitContact::%s\n", strerror(errno));
    return 1;
  }
  pc->capacity = DEFAULT_SZ;
  return 0;
}
void DestroyContact(Contact* pc)
{
  assert(pc);
  free(pc->data);
  pc->data = NULL;
}
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 1;
    }
    else
    {
      pc->data = ptr;
      pc->capacity += INC_SZ;
      printf("增容成功\n");
    }
  }
}
void AddContact(Contact* pc)
{
  assert(pc);
  //增容
  CheckCapacity(pc);
  printf("请输入名字:》");
  scanf("%s", pc->data[pc->count].name);
  printf("请输入年龄:》");
  scanf("%d", &(pc->data[pc->count].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%-5d\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);
  }
}
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 DelContact(Contact* pc)
{
  char name[MAX_NAME] = { 0 };
  assert(pc);
  int i = 0;
  if (pc->count == 0)
  {
    printf("通讯录为空,没有信息可以删除\n");
    return;
  }
  printf("请输入要删除人的名字:>");
  scanf("%s", name);
  //删除
  //1.查找
  int pos = FindByName(pc, name);
  if (pos == -1)
  {
    printf("要删除的人不存在\n");
    return;
  }
  //2.删除
  for (i = pos; i < pc->count; i++)
  {
    pc->data[i] = pc->data[i + 1];
  }
  pc->count--;
}
void SeachContact(Contact* pc)
{
  assert(pc);
  char name[MAX_NAME] = { 0 };
  printf("请输入需要查找的联系人的名字:>");
  scanf("%s", name);
  //1.查找
  int pos = FindByName(pc, name);
  if (pos == -1)
  {
    printf("要查找的人不存在\n");
    return;
  }
  //2.打印
  printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n", "名字", "年龄", "性别", "电话", "地址");
  printf("%-20s\t%-5d\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] = { 0 };
  printf("请输入需要查找的联系人的名字:>");
  scanf("%s", name);
  //1.查找
  int pos = FindByName(pc, name);
  if (pos == -1)
  {
    printf("要查找的人不存在\n");
    return;
  }
  printf("要修改人的信息已经找到,接下来进行修改\n");
  //2.修改
  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_by_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_by_name);
  printf("排序成功\n");
}

test.c

通讯录主脉络:

#define _CRT_SECURE_NO_WARNINGS
#include "contact.h"
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 1:
      AddContact(&con);
      break;
    case 2:
      DelContact(&con);
      break;
    case 3:
      SeachContact(&con);
      break;
    case 4:
      ModifyContact(&con);
      break;
    case 5:
      ShowContact(&con);
      break;
    case 6:
      SortContact(&con);
      break;
    case 0:
      DestroyContact(&con);
      printf("退出通讯录\n");
      break;
    default:
      printf("选择错误\n");
    }
  } while (input);
  return 0;
}


目录
相关文章
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
21天前
|
C语言
【c语言】动态内存管理
本文介绍了C语言中的动态内存管理,包括其必要性及相关的四个函数:`malloc`、``calloc``、`realloc`和`free`。`malloc`用于申请内存,`calloc`申请并初始化内存,`realloc`调整内存大小,`free`释放内存。文章还列举了常见的动态内存管理错误,如空指针解引用、越界访问、错误释放等,并提供了示例代码帮助理解。
33 3
|
1月前
|
编译器 程序员 C语言
深入C语言:动态内存管理魔法
深入C语言:动态内存管理魔法
|
1月前
|
存储 C语言
探索C语言数据结构:利用顺序表完成通讯录的实现
本文介绍了如何使用C语言中的顺序表数据结构实现一个简单的通讯录,包括初始化、添加、删除、查找和保存联系人信息的操作,以及自定义结构体用于存储联系人详细信息。
19 2
|
1月前
|
存储 程序员 编译器
C语言——动态内存管理与内存操作函数
C语言——动态内存管理与内存操作函数
|
23天前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
20 0
|
1月前
|
C语言
保姆级教学 - C语言 之 动态内存管理
保姆级教学 - C语言 之 动态内存管理
19 0
|
1月前
|
存储 C语言
深入C语言内存:数据在内存中的存储
深入C语言内存:数据在内存中的存储
|
1月前
|
C语言 C++
c语言回顾-内存操作函数
c语言回顾-内存操作函数
40 0
|
1月前
|
存储 C语言 C++
来不及哀悼了,接下来上场的是C语言内存函数memcpy,memmove,memset,memcmp
本文详细介绍了C语言中的四个内存操作函数:memcpy用于无重叠复制,memmove处理重叠内存,memset用于填充特定值,memcmp用于内存区域比较。通过实例展示了它们的用法和注意事项。
64 0