13.动态内存管理

0.前言

为什么存在动态内存分配?

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
这样静态开辟的变量众口难调,旱的旱死,涝的涝死
  1. 空间开辟大小是固定的。
  2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

动态内存分配所申请的空间都是在堆区上的

1.动态内存函数

malloc

void *malloc( size_t size );

malloc returns a void pointer to the allocated space, or NULL if there is insufficient memory available. 
To return a pointer to a type other than void, use a type cast on the return value. The storage space pointed to by the return value is guaranteed to be suitably aligned for storage of any type of object. If size is 0, malloc allocates a zero-length item in the heap and returns a valid pointer to that item. Always check the return from malloc, even if the amount of memory requested is small.
malloc返回一个指向已分配空间的void指针,如果可用内存不足,则返回NULL。若要返回指向非void类型的指针,请对返回值进行类型转换。返回值所指向的存储空间保证对任何类型的对象进行适当的对齐。如果size为0,malloc会在堆中分配一个长度为0的项,并返回一个指向该项的有效指针。总是检查malloc的返回值,即使请求的内存量很小


#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
	//开辟10个整型空间
	int* p = (int*)malloc(4000000000000000);
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));//Not enough space
	}
	return 0;
}

free

void free( void *memblock );
Previously allocated memory block to be freed
    开辟的空间如果不释放,那么别人也用不上的
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
	//开辟10个整型空间
	int* p = (int*)malloc(40);//malloc返回的是void*
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));//Not enough space
	}

	//使用
	if (p)//需要检测指针有效性  或者在上面那个if里面加上return;
	{
		for (size_t i = 0; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (size_t i = 0; i < 10; i++)
		{
			printf("%d ", p[i]);//0 1 2 3 4 5 6 7 8 9
		}
	}

	//释放
	free(p);//释放空间后,p保存的还是那个地址,这样p就变成了野指针
	p = NULL;//所有释放p指向的空间后要把p置为NULL
	return 0;
}

calloc

void *calloc( size_t num, size_t size );

Allocates an array in memory with elements initialized to 0.
    
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

int main()
{
	//开辟10个整型空间
	int* p = (int*)calloc(10,sizeof(int));//calloc返回的也是void*
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));//Not enough space
	}

	//使用
	if (p)
	{
		/*for (size_t i = 0; i < 10; i++)
		{
			*(p + i) = i;
		}*/
		for (size_t i = 0; i < 10; i++)
		{
			printf("%d ", p[i]);//0 0 0 0 0 0 0 0 0 0
		}
	}

	//释放
	free(p);//释放空间后,p保存的还是那个地址,这样p就变成了野指针
	p = NULL;//所有释放p指向的空间后要把p置为NULL
	return 0;
}

realloc

void* realloc (void* ptr, size_t size);

Reallocate memory blocks.

1.可以开辟空间
2.也可以调整空间
    Pointer to a memory block previously allocated with malloc, calloc or realloc.Alternatively, this can be a null pointer, in which case a new block is allocated (as if malloc was called).
     
        
realloc如果发现要原来的空间后面不够扩充时,会重新找个空间开辟,并把原来空间的值拿下来,并释放原来的空间
如果realloc调整失败,返回空指针
千万不能这么写:p = (int*)realloc(p, 80);
这有可能导致原始内存块泄露
    这样写才行
    int* ptr = (int*)realloc(p, 80);
	if (NULL != ptr)
	{
		p = ptr;
	}

image-20220124222106205

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

int main()
{
	//开辟10个整型空间
	int* p = (int*)calloc(10, sizeof(int));//calloc返回的也是void*
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));//Not enough space
	}

	//使用
	if (p)
	{
		for (size_t i = 0; i < 10; i++)
		{
			printf("%d ", p[i]);//0 0 0 0 0 0 0 0 0 0
		}
	}
	//空间不够,需要扩容
	int* ptr = (int*)realloc(p, 80);
	if (NULL != ptr)
	{
		p = ptr;
	}

	//释放
	free(p);//释放空间后,p保存的还是那个地址,这样p就变成了野指针
	p = NULL;//所有释放p指向的空间后要把p置为NULL
	return 0;
}
r
//开辟10个整型空间
	int* p = (int*)realloc(NULL, 40);//realloc 如果传NULL 那就与malloc等价
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));//Not enough space
	}

2.常见错误

对NULL指针的解引用

#include<stdio.h>
#include<limits.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(INT_MAX);//#define INT_MAX       2147483647
	int i = 0;
	for ( i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	return 0;
}
#include<stdio.h>
#include<limits.h>
#include<stdlib.h>
#include<assert.h>
int main()
{
	int* p = (int*)malloc(INT_MAX);//#define INT_MAX       2147483647
	assert(p);//Assertion failed: p,  如果开辟失败,返回空指针
	if (p)
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;
		}
	}
	return 0;
}
或者这样修改:
if (p == NULL)
{
    printf("%s\n", strerror(errno));
    return 0;
}

//使用
int i = 0;
for (i = 0; i < 10; i++)
{
    *(p + i) = i;
}

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

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

	//使用
	for (size_t i = 0; i <= 10; i++)
	{
		*(p + i) = 'a' + 1;//e
	}

	//释放
	free(p);
	p = NULL;

	return 0;
}

image-20220124230751537

对非动态开辟的内存使用了free函数

void test()
{
    int a = 10;
    int *p = &a;
    free(p);//err
}

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

void test()
{
    int *p = (int *)malloc(100);
    p++;
    free(p);//p不再指向动态内存的起始位置 p找不到起始位置就会有内存泄漏的风险
    //err
}

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

void test()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p);//重复释放  程序会卡死
}
void test()
{
    int *p = (int *)malloc(100);
    free(p);
    p = NULL;
    free(p);//重复释放,free空指针没事,但最好还是不用重复free
}

动态开辟内存忘记释放(内存泄漏)

void test()
{
    int *p = (int *)malloc(100);
    if(NULL != p)
    {
        *p = 20;
    }

    //忘记释放,就会出现内存泄露问题  别人用不了那块空间了
}
int main()
{
    test();
    while(1);
}
//谁申请谁释放

3.笔试题

1.

void GetMemory(char *p) 
{
    p = (char *)malloc(100);
}
void Test(void) 
{
    char *str = NULL;
    GetMemory(str);//传值调用
    strcpy(str, "hello world");//str仍是空指针,此时是非法访问内存,程序会崩溃
    printf(str);//这样写是可以的
}
int main()
{
    Test();
    return 0;
}
//运行结果:程序崩溃,什么都没有
char* p = "hello world";//实际上放的就是首地址
printf("hello world");
printf(p);//传的是首地址,可以这样写的
修改:传址调用即可
void GetMemory(char** p)
{
    *p = (char*)malloc(100);
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str);
    strcpy(str, "hello world");
    printf(str);//这样写是可以的

    //释放
    free(str);
    str = NULL;
}
int main()
{
    Test();//hello world
    return 0;
}

char* GetMemory(char* p)
{
    p = (char*)malloc(100);//动态申请的空间不会被销毁
    return p;
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory(str);
    strcpy(str, "hello world");
    printf(str);//这样写是可以的

    //释放
    free(str);
    str = NULL;
}
int main()
{
    Test();//hello world
    return 0;
}
//不传地址,return指针也行

"注意:p虽然会销毁,但销毁之前是把p放到寄存器里面的"
    
    动态内存函数申请的空间是在堆区的,不会像局部变量那样被销毁

2.

//返回栈空间地址问题
//注意与返回变量本身不同
char *GetMemory(void) 
{
    char p[] = "hello world";//局部变量会被销毁
    return p; //函数结束时p会被销毁了,p是地址,地址是可以返回回去,但地址指向的那片空间已经被销毁了,没有用了
}
void Test(void) 
{
    char *str = NULL;
    str = GetMemory();
    printf(str);//随机值,烫烫烫....
}
int main()
{
    Test();//
    return 0;
}


int test()
{
    int a = 10;
    return a;//返回变量本身是可以的,因为返回的时候会把a的值放到寄存器里面去,再把寄存器的值返回给m
}
int main()
{
    int m = test();
    printf("%d\n",m);
    return 0;
}

image-20220125230148719

int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);//打印出10纯属侥幸,随机值
	return 0;
}
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("hehe\n");//此时再为这句printf申请的空间就会把a原来所在的空间覆盖掉
	printf("%d\n", *p);//此时打印出的就是另一个随机值
	return 0;
}

image-20220125230532571

image-20220125230823421

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);

    //忘记free了
    //应该加上:
    free(str);
    str = NULL;
}

4.

void Test(void) 
{
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);
    if(str != NULL)
    {
        strcpy(str, "world");//之前对str指向的空间已经释放,不能再次使用,形成非法访问内存
        //因此,应该及时把str置成NULL
        printf(str);
    }
}

4.C/C++的内存开辟

4.C/C++程序的内存开辟

image-20220127090843358

常量字符串放在代码段内
内核空间到代码段,是高地址到低地址

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。

  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

5.柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

//柔性数组2种写法
struct S1
{
	int n;
	int arr[0];//大小未指定
};
struct S2
{
	int n;
	int arr[];//大小未指定
};

int main()
{
	printf("%d\n", sizeof(struct S1));//4
	printf("%d\n", sizeof(struct S2));//4
	return 0;
}

特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
struct S2
{
	int n;
	int arr[];//大小未指定
};

int main()
{
	printf("%d\n", sizeof(struct S1));//4
	printf("%d\n", sizeof(struct S2));//4
	struct S2* ps = (struct S2*)malloc(sizeof(struct S2) + 40);
	ps->n = 100;
	for (size_t i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//...
	//增容
	struct S2* ptr = (struct S2*)realloc(ps, sizeof(struct S2) + 80);
	if (ptr == NULL)
	{
		return 0;
	}
	else
	{
		ps = ptr;
	}

	//继续使用

	//只需要1次释放
	free(ps);
	ps = NULL;
	return 0;
}

对比:

struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	ps->n = 100;
	ps->arr = (int*)malloc(40);//都是在堆上开辟的
	//使用
	//增容

	//2次释放
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;

	return 0;
}

image-20220127105446615

如果柔性数组前面还有n多个变量呢?他们的地址空间就一定会连续吗?不见得吧,所以柔性数组还是有其必要的

而且柔性数组开辟和释放次数少,不容易出错
开辟次数越多,产生的内存碎片也就越多

并且连续的内存有利于提高访问速度

6.通讯录

test.c

#define _CRT_SECURE_NO_WARNINGS 1
//1. 存放1000个人的信息
//2. 人的相关信息,名字,年龄,电话,住址,性别
//3. 增删查改
//4. 排序

#include"contact.h"
enum Option
{
	EXIT,
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SORT,
	SHOW,
	CLS
};
void menu()
{
	printf("***************************************\n");
	printf("********     1.add      2.del     *****\n");
	printf("********     3.search   4.modify  *****\n");
	printf("********     5.sort     6.show    *****\n");
	printf("********     7.cls      0.exit    *****\n");
	printf("***************************************\n");
}

int main()
{
	int input = 0;
	static Contact con;//通讯录
	//初始化通讯录
	InitContact(&con);
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case ADD:
			AddContact(&con);
			break;
		case DEL:
			DeleteContact(&con);
			break;
		case SEARCH:
			SearchContact(&con);
			break;
		case MODIFY:
			ModifyContact(&con);
			break;
		case SORT:
			SortContact(&con);
			break;
		case SHOW:
			ShowContact(&con);
			break;
		case CLS:
			ClsContact();
			break;
		case EXIT:
			//回收通讯录
			DestroyContact(&con);
			break;
			printf("退出通讯录\n");
		default:
			printf("选择错误\n");
			break;
		}
	}while(input);
	return 0;
}

contact.h

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<memory.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#include<errno.h>
#include<Windows.h>

#define MAX 1000
#define NAME_MAX 20
#define SEX_MAX 5
#define ADDR_MAX 30
#define TELE_MAX 12
#define DEFAULT_SZ 3//默认存储的人数信息
typedef struct Peoinfo
{
	char name[NAME_MAX];
	int age;
	char sex[SEX_MAX];
	char addr[ADDR_MAX];
	char tele[TELE_MAX];
}Peoinfo;

//通讯录的结构体
typedef struct Contact
{
	//Peoinfo data[MAX];
	Peoinfo* data;
	int sz;//通讯录中有效信息的个数
	int capacity;//记录当前通讯录的最大容量
}Contact;

//初始化通讯录
void InitContact(Contact* pc);
//销毁通讯录
void DestroyContact(Contact* pc);
//添加联系人信息
void AddContact(Contact* pc);
//展示通讯录
void ShowContact(const Contact* pc);
//删除通讯录
void DeleteContact(Contact* pc);
//查找联系人
void SearchContact(Contact* pc);
//修改联系人
void ModifyContact(Contact* pc);
//排序联系人
void SortContact(Contact* pc);
//清屏
void ClsContact();

contact.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"

void InitContact(Contact* pc)
{
	assert(pc);
	pc->sz = 0;
	//data是一片连续的空间
	//memset(pc->data, 0, sizeof(pc->data));
	Peoinfo* tmp = (Peoinfo*)malloc(DEFAULT_SZ * sizeof(Peoinfo));
	if (NULL != tmp)
	{
		pc->data = tmp;
	}
	else
	{
		printf("InitContact()::%s\n", strerror(errno));
		return;
	}
	pc->capacity = DEFAULT_SZ;
}

void DestroyContact(Contact* pc)
{
	assert(pc);
	free(pc->data);
	pc->data = NULL;
	pc->sz = 0;
	pc->capacity = 0;
}



void check_capacity(Contact* pc)
{
	assert(pc);
	if (pc->sz == pc->capacity)
	{
		//扩容
		Peoinfo* tmp = (Peoinfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(Peoinfo));
		if (NULL != tmp)
		{
			pc->data = tmp;
			pc->capacity += 2;
			printf("扩容成功\n");
		}
		else
		{
			printf("Check_Capacity()::%s\n", strerror(errno));
		}
	}
}
void AddContact(Contact* pc)
{
	assert(pc);
	
	//检测是否扩容
	check_capacity(pc);

	//输入联系人
	printf("请输入名字:>\n");
	scanf("%s", pc->data[pc->sz].name);
	printf("请输入年龄:>\n");
	scanf("%d", &(pc->data[pc->sz].age));
	printf("请输入性别:>\n");
	scanf("%s", pc->data[pc->sz].sex);
	printf("请输入电话:>\n");
	scanf("%s", pc->data[pc->sz].tele);
	printf("请输入地址:>\n");
	scanf("%s", pc->data[pc->sz].addr);

	pc->sz++;
	printf("增加联系人成功\n");
	Sleep(1000);
	system("cls");
}

void ShowContact(const Contact* pc)
{
	assert(pc);
	int i = 0;
	//负号表示左对齐
	printf("%-20s\t%-5s\t%-5s\t%-13s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
	for (i = 0; i < pc->sz; i++)
	{
		printf("%-20s\t%-5d\t%-5s\t%-13s\t%-20s\n",
			pc->data[i].name, pc->data[i].age,
			pc->data[i].sex, pc->data[i].tele,
			pc->data[i].addr);
	}
	printf("显示成功\n");
}

int FindByName(const Contact* pc, char name[])
{
	int i = 0;
	for ( i = 0; i < pc->sz; i++)
	{
		if (strcmp(pc->data[i].name,name) == 0)
		{
			return i;
		}
	}
	return -1;//找不到
}

void DeleteContact(Contact* pc)
{
	char name[NAME_MAX] = { 0 };
	assert(pc);
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法删除\n");
		return;
	}
	printf("请输入要删除人的名字:>\n");
	scanf("%s", name);
	//查找指定联系人
	int pos = FindByName(pc, name);//下标
	if (pos == -1)
	{
		printf("要删除的人不存在\n");
		return;
	}
	else
	{
		//删除
		int j = 0;
		for ( j = pos; j < pc->sz-1; j++)
			//sz-1 如果sz是999 访问到最大的就是998,998+1没有越界
		{
			pc->data[j] = pc->data[j + 1];
		}
		pc->sz--;
		printf("删除指定联系人成功\n");
	}
}

void SearchContact(Contact* pc)
{
	char name[NAME_MAX] = { 0 };
	assert(pc);
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法查找\n");
		return;
	}
	printf("请输入要查找人的名字:>\n");
	scanf("%s", name);
	//查找指定联系人
	int pos = FindByName(pc, name);//下标
	if (pos == -1)
	{
		printf("要查找的人不存在\n");
		return;
	}
	else
	{
		//负号表示左对齐
		printf("%-20s\t%-5s\t%-5s\t%-13s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
		printf("%-20s\t%-5d\t%-5s\t%-13s\t%-20s\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)
{
	char name[NAME_MAX] = { 0 };
	assert(pc);
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法修改\n");
		return;
	}
	printf("请输入要修改人的名字:>\n");
	scanf("%s", name);
	//查找指定联系人
	int pos = FindByName(pc, name);//下标
	if (pos == -1)
	{
		printf("要修改的人不存在\n");
		return;
	}
	else
	{
		//修改联系人信息
		printf("请输入名字:>\n");
		scanf("%s", pc->data[pos].name);
		printf("请输入年龄:>\n");
		scanf("%d", &(pc->data[pos].age));
		printf("请输入性别:>\n");
		scanf("%s", pc->data[pos].sex);
		printf("请输入电话:>\n");
		scanf("%s", pc->data[pos].tele);
		printf("请输入地址:>\n");
		scanf("%s", pc->data[pos].addr);

		printf("修改联系人成功\n");
	}
}
int cmp_con_by_age(const void* e1, const void* e2)
{
	return (((Peoinfo*)e1)->age - ((Peoinfo*)e2)->age);
}

void SortContact(Contact* pc)
{
	assert(pc);
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法排序\n");
		return;
	}
	qsort(pc->data, pc->sz, sizeof(Peoinfo), cmp_con_by_age);
	printf("排序完成:\n");
	ShowContact(pc);
}



void ClsContact()
{
	Sleep(1000);
	system("cls");
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在C++中,动态分配内存可能会导致以下问题: 1. 内存泄漏:如果在动态分配内存后没有正确释放,就会导致内存泄漏。内存泄漏指的是程序在运行过程中动态分配的内存没有被释放,导致系统内存资源逐渐耗尽。 2. 内存访问越界:在动态分配内存时,如果没有正确计算所需的内存大小,或者在使用指针时操作越界,就会导致内存访问越界问题。这可能会导致程序崩溃或产生未定义行为。 3. 野指针:野指针是指指向已释放或未分配内存的指针。如果在动态分配内存后,没有将指针置空或释放前将其赋值给其他有效的指针,就有可能产生野指针。对野指针进行解引用操作会导致程序崩溃或产生未定义行为。 4. 多次释放内存:在动态分配内存后,如果多次使用delete或free释放同一块内存,会导致程序崩溃或产生未定义行为。这种错误可能是由于逻辑错误、指针复制不当或双重释放等原因引起的。 5. 内存碎片化:频繁的动态分配和释放内存可能导致内存碎片化问题。内存碎片化指的是内存空间被划分成多个小片段,无法有效地分配大块连续内存。这可能会导致内存分配效率降低。 为了避免这些问题,在使用动态分配内存时,应该始终确保正确地释放内存、避免内存访问越界、及时将指针置空、避免多次释放同一块内存,并尽量减少频繁的动态内存分配和释放操作。此外,可以使用智能指针等RAII(资源获取即初始化)技术来帮助管理动态分配的内存,以提高代码的健壮性和可维护性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值