c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理

news2025/4/16 11:02:59

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理

【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理


文章目录

  • c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理
  • 前言
  • 一、为什么存在动态内存分配
  • 二、动态内存函数的介绍
    • 1. malloc和free
    • 2. calloc
    • 3. realloc
  • 三、常见的动态内存错误
    • 1. 对NULL指针的解引用操作
    • 2. 对动态开辟空间的越界访问
    • 3. 对非动态开辟内存使用free释放
    • 4. 使用free释放一块动态开辟内存的一部分
    • 5. 对同一块动态内存多次释放
    • 6. 动态开辟内存忘记释放(内存泄漏)
  • 四、使用动态内存修改通讯录
  • 五、几个经典笔试题
    • 1. 题目1
    • 2. 题目2
    • 3. 题目3
    • 4. 题目4
  • 六、C/C++程序的内存开辟
  • 七、柔性数组
    • 1. 柔性数组的特点
    • 2. 柔性数组的使用
    • 3. 柔性数组的优势
  • 总结


前言

大家在使用数组时肯定都会有过这样的疑问,如果一个数组的长度不足了,可以延长这个数组吗?使其能够满足存储的需求。又或者是现在不确定数组应该设为多长,先把它放着,等之后再设置它的长度。
上面的这些需求,数组显然是无法实现的,那么c语言中有什么东西能够做到这一点呢?
动态内存分配!你现在不知道需要多大的空间,没关系,之后你明确了的时候,使用动态内存分配,将你要的空间分配给即可。

本章重点:

  • 为什么存在动态内存分配
  • 动态内存函数的介绍
    • malloc
    • free
    • calloc
    • realloc
  • 常见的动态内存错误

一、为什么存在动态内存分配

此前我们掌握的内存开辟方式有:

int val = 20; // 在栈空间开辟4个字节的空间
char arr[10] = { 0 }; // 在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

  • 空间开辟大小的固定的;
  • 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但正如前言中说的,在处理实际问题时,我们对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小只有在程序运行的时候才知道,这时候就只能试试动态内存开辟了。

二、动态内存函数的介绍

1. malloc和free

c语言提供了一个动态内存开辟的函数malloc

void* malloc(size_t size);

在这里插入图片描述

  • 这个函数的功能是分配一块内存空间,并返回这片空间的起始地址;
  • 这片空间的大小由参数size指定,单位为字节;
  • 这片新分配的空间并未初始化,保存的数据未知;
  • 如果参数size为0时,这种行为未被定义,取决于编译器;
  • 如果该函数分配空间成功,则会返回这块空间的地址;这个指针的类型为void *是因为它可以强制转换为任何所需的类型以满足解引用的需求;如果分配空间失败则会返回一个NULL指针;

c语言还提供了另一个函数free

void free(void*)

在这里插入图片描述

  • 它的功能是回收一片动态分配的内存空间;
  • 它可以回收malloccalloc或者是realloc函数分配的空间,使得这片空间又能被再次分配;
  • 如果`ptr``指向的空间不是动态分配的内存空间,这种行为是未定义的;
  • 如果ptr是一个NULL指针,这个函数什么都不会做(不是错误);
  • 注意,这个函数并不会改变ptr这个指针本身的内容,也就是说指针ptr仍指向这片空间(变成了野指针);

举个例子:

#include <stdio.h>
#include <stdlib.h> // 动态分配函数必须包含库

int main()
{
	// 代码1
	int num = 0;
	scanf("%d", &num);
	// 要在程序执行过程中,从用户处获得输入,设置一个用户指定大小的数组
	int arr[num] = { 0 }; // error,数组的定义[]中的值应该是一个常量

	// 代码2
	int* ptr = NULL;
	ptr = (int*)malloc(num * sizeof(int)); // malloc函数的单位为字节
	if (ptr == NULL) // 内存分配失败
	{
		perror("malloc"); // 输出错误信息,方便调试
		return;
	}
	// 内存分配成功后执行逻辑
	int i = 0;
	for (i = 0; i < num; i++)
	{
		*(ptr + i) = 0;
	}

	// 内存使用完毕
	// 回收空间
	free(ptr);
	ptr = NULL; // free函数并不会改变ptr本身的值,需要程序员手动将指针置空

	return 0;
}

在这里插入图片描述

注意
在最后的ptr = NULL这条语句是非常有必要的,因为没有这条语句,ptr此时已经变成了野指针,这是十分危险的;

2. calloc

除了能使用malloc函数进行内存分配之外,c语言还提供了calloc函数来进行内存分配;

void* calloc(size_t num, size_t size);

大家可能会疑惑,我们已经有malloc函数,为什么还要来一个calloc呢?
请接着往下看:
在这里插入图片描述

  • 这个函数的功能是分配一片空间,并对这片空间进行初始化(初始化为0);
  • calloc将会为num个元素分配一片内存空间,每个元素占size个字节,并在分配空间之后,以字节为单位将这片空间的值初始化为0;
  • 这个函数的有效结果就是分配一片初值为0的,有num * size这么多字节的空间;
  • 如果size为0,它的返回值依赖于特定的库实现(不一定是NULL指针),但这个指针一定不能被解引用;
  • 两个参数,num表示元素的个数,size表示一个元素占的字节数;
  • 如果分配成功,函数会返回一个指向这片空间的指针,指针的类型为void *,它可以根据需求,由使用者自由的强制转换成需要的类型,并进行解引用操作;
  • 如果分配失败,将会返回一个NULL指针;

从上面的解读中,我们就能知道,malloc函数仅仅只是分配了这么一片空间,但其中的值是没有被初始化的,calloc函数能够在分配空间的同时,将这片空间的每个字节都初始化为0;

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

int main()
{
	int *ptr1 = (int*)malloc(10 * sizeof(int));
	if (ptr1 == NULL)
	{
		perror("malloc");
		return;
	}

	int *ptr2 = (int*)calloc(10, sizeof(int));
	if (ptr2 == NULL)
	{
		perror("calloc");
	}

	free(ptr1);
	ptr1 == NULL;
	free(ptr2);
	ptr2 = NULL;

	return 0;
}

在这里插入图片描述
所以当我们对申请的空间的内容有初始化的要求时,就可以使用calloc函数很方便的完成了;

3. realloc

有了上面的两个内存分配函数,我们能够在程序执行中根据需求为变量分配内存空间;
但是仔细想想,当我们在分配完空间之后,这片空间仍不足以满足需求呢?这时应该怎么办?
是再使用malloccalloc函数重新分配一片空间,并将原来的空间回收吗?
这样是否有点麻烦,有没有其他方法?
有的,兄弟有的。
使用realloc函数;

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

在这里插入图片描述

  • realloc函数可以重新分配动态分配的空间,改变ptr指向空间的大小;
  • 这个函数可能会分配一片新的内存空间(返回值指向的空间);
  • 这片空间的内容会保留到新大小和旧大小中较小的那个,即使这片空间是一片新分配的空间;如果新大小更大,新分配空间中的内容是未定义的;
  • 如果参数ptrNULL指针,这个函数的行为和malloc相同,分配一片大小为size字节的空间,并返回一个指向它的指针;
  • 在C90标准中,如果参数size为0,调用该函数的行为就像调用了free一样,它会释放掉ptr指向的空间,并返回一个NULL指针;在C99/C11标准中,则取决于特定的库实现,该行为未定义;
  • 返回值有两种情况:
    1. 返回的指针和ptr相同;
    2. 返回的指针和ptr不同,是一个新的地址;
  • 返回指针的类型为void*,原因和前面的相同;
  • C90标准中,有两种情况会返回NULL指针:参数size为0;空间分配失败;
  • C99/C11标准中,只有空间分配失败会返回NULL指针。参数size为0的这种行为未定义;

在这里插入图片描述

看下面代码:

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

int main()
{
	int* ptr = (int*)malloc(100);
	if (ptr == NULL)
	{
		perror("malloc");
		return;
	}
	// 业务处理

	// 扩充容量
	// 代码1
	ptr = (int*)realloc(ptr, 1000); 

	// 代码2
	int* p = (int*)realloc(ptr, 1000);
	if (p == NULL)
	{
		perror("realloc");
		return;
	}
	ptr = p;
	
	// 业务处理

	free(ptr);
	ptr = NULL;

	return 0;
}

上面代码中代码1和代码2正确吗,应该用哪个?

根据上面的函数解读,我们知道realloc函数是可能出现分配失败的情况的。
在代码1中,如果realloc失败,它会返回NULL指针,此时直接将ptr设为了NULL指针,这就导致了内存泄漏,之后再也找不到ptr指向的空间了;
所以代码1是不可行的,应该使用代码2,在调用realloc之后,先判断它的返回值,看看空间是否分配成功,成功之后,才将分配的地址赋值给ptr;

三、常见的动态内存错误

1. 对NULL指针的解引用操作

#include <limits.h>

void test()
{
	int* p = (int *)malloc(INT_MAX / 4);
	*p = 20;
	free(p);
	p = NULL;
}

这段代码正确吗?
错误,因为malloc函数可能返回NULL指针,当空间分配失败时,ptr指针为NULL指针,对NULL指针解引用是错误的;

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

#include <stdio.h>

void test()
{
	int i = 0;
	int *p = (int *)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return;
	}

	for (i = 0; i < 11; i++)
	{
		*(p + i) = 1;
	}

	free(p);
	p = NULL;
}

这段代码也是有问题的,
malloc函数只为p分配了10个int类型的空间,循环会访问到第十一个int类型的元素,此时产生了越界;

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

#include <stdlib.h>

void test()
{
	int arr[10] = { 0 };
	int *p = arr;
	free(p);
}

这段代码就是使用了free函数释放不是动态分配的内存空间,此时也会出错;

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

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

void test()
{
	int *p = (int *)malloc(10 * sizeof(int));
	if (p == NULL)
	{	
		perror("malloc");
		return ;
	}

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p++) = i;
	}

	free(p);
	p = NULL;
}

这段代码同样是有问题的,在循环中,指针p指向的位置已经发生了变化,此时指针p并没有指向动态分配的内存空间的起始位置,此时使用free函数来释放动态分配的空间,仍有一部分没有释放,所以这个代码是错误的;

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

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

void test()
{
	int *p = (int *)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return ;
	}

	// 业务实现
	free(p);

	// 业务实现
	free(p);
}

这种代码也是错误的,它对同一片动态内存空间释放了两次;
虽然,上面的代码不太可能会出现(毕竟没有谁会在同一个函数中对一片动态分配的内存空间释放多次),但这样的错误的可能会出现的;
动态分配的空间,只有两种情况会释放,程序结束、手动free;有可能出现,你在这个函数中将这个空间释放了一次,在另一个函数中又对它释放了一次,这时就出现了多次释放的错误;

有方法可以规避这种错误:

free(p);
p = NULL;

上面的这两行代码就能规避多次释放的问题,还记得free函数的特点吗?
当free函数的参数为NULL指针时,调用这个函数什么也不会发生。
在每次释放指针指向的动态内存空间之后,紧接着将这个指针置为NULL指针,能够有效的规避这种错误的出现;

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

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

void test()
{
	int *p = (int *)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return ;
	}

	// 业务处理

	// 忘记释放动态内存空间
	// 出现内存泄漏
}

int main()
{
	test();
	// 业务处理

	return 0;
}

上述代码中,主函数调用了test函数,动态分配了一个内存空间给指针p,在test函数退出时,并没有释放这片空间,这片空间就一直留存在堆区中,且指针p是test函数的局部变量,在test弹出之后,这个变量就销毁了,这片动态分配的空间也就没有人能够找到了,所以这片空间在之后的程序中是无法再被使用的,只有在整个程序结束时,这片空间才会还给OS,这就产生了内存泄漏问题。
可能有人会这没什么,反正程序结束,这片空间会归还给OS的,我们不用管;
但是试想一下,这个程序如果是一个服务器程序呢?它需要长期的执行,每次调用test函数都会泄漏一部分的内存空间,硬件资源总是有限的,那么这个程序在运行一段时间之后就一定会因为内存资源不足而挂掉,如果2天这个程序就把内存消耗光了,就得2天重启一次程序,这会带来很大的损失;

忘记释放不再使用的动态开辟的空间会造成内存泄漏
切记
动态开辟的空间一定要释放,并且要正确的释放

四、使用动态内存修改通讯录

在之前的通讯录结构体类型中,不管什么情况,通讯录都能保存1000个人的信息,如果这个人他只有100个朋友的信息需要保存,他使用这个通讯录会浪费剩下的900个空间,这会造成内存资源很大的浪费;如果他需要保存的信息超过1000个,那么这个通讯录就无法满足他的需求;
这时我们就可以使用动态内存来替换掉之前使用的静态分配的空间,在程序执行过程中,根据用户的需要增加通讯录的空间,即满足了用户要保存人的信息的需求,又减少了系统资源的开销;

// 静态版本
// 定义结构体、标识符常量
#define MAXNUM 1000 // 通讯录中能保存的人数

typedef struct
{
	int age; // 年龄
	char name[20]; // 名字
	char sex[7]; // 性别
	char phone_number[20]; // 电话号码
	char address[30]; // 地址
} Peoinfo; // 保存人的信息

typedef struct
{
	Peoinfo data[MAXNUM]; // 通讯录可以保存1000个人的信息
	int count; // 当前通讯录中的人数
} Contact;

修改后的代码,有改动的代码

// 动态内存版本
#define INIT_COUNT 3 // 通讯录初始能保存3个人的信息
#define INCREASE_COUNT 2 // 每次扩容增加数量

typedef struct
{
	int age; // 年龄
	char name[20]; // 名字
	char sex[7]; // 性别
	char phone_number[20]; // 电话号码
	char address[30]; // 地址
} Peoinfo; // 保存人的信息

// 使用指针指向保存人的信息的空间
typedef struct
{
	Peoinfo* data; // 指向保存人信息的空间
	int count; // 当前通讯录中的人数
	int num; // 当前通讯录中能保存的最大人数
} Contact;
void initContact(Contact* contact)
{
	assert(contact);

	// 动态分配 INIT_COUNT * sizeof(Peoinfo) 个字节的空间
	contact->data = (Contact*)calloc(INIT_COUNT, sizeof(Peoinfo));
	contact->count = 0;
	contact->num = INIT_COUNT;
}

static void increase_Capacity(Contact* contact)
{
	// 为通讯录的保存人数扩容
	Contact* ptr = (Contact*)realloc(contact->data, ((contact->num) + INCREASE_COUNT) * sizeof(Peoinfo));
	// 空间分配失败
	if (ptr == NULL)
	{
		perror("checkCapacity, realloc");
		return;
	}
	contact->data = ptr; // 将重新分配的空间的地址赋值给contact
	contact->num += INCREASE_COUNT;
}

void Add(Contact* contact)
{
	assert(contact);
	if (isFull(contact))
	{
		increase_Capacity(contact);
		printf("已扩容\n");
	}

	getchar();
	// 没有做溢出检查
	printf("输入名字:>");
	gets((contact->data[contact->count]).name);

	printf("输入年龄:>");
	scanf("%d", &((contact->data[contact->count]).age));

	getchar();
	printf("输入性别:>");
	gets((contact->data[contact->count]).sex);

	//getchar();
	printf("输入电话:>");
	gets((contact->data[contact->count]).phone_number);

	//getchar();
	printf("输入地址:>");
	gets((contact->data[contact->count]).address);

	(contact->count)++;
}

别忘了要手动释放动态开辟的内存空间

case exit:
	free(contact.data);
	contact.data = NULL;
	printf("退出通讯录\n");
	break;

五、几个经典笔试题

1. 题目1

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

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test()
{
	char *str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

int main()
{
	// 调用Test函数会有什么样的结果?
	Test();

	return 0;
}

分析:
根据代码意思,这段代码的目的是在Test函数中定义了一个char *的变量,初始化为NULL指针,调用GetMemorystr动态分配一片空间,之后将“hello world”这个字符串复制到这片动态开辟的空间中;
但事实真的如此吗?
来看看运行结果:
在这里插入图片描述
为什么会这样呢?
我们不是将一个指针作为参数吗?这不是传址调用吗?
真的吗?真的是传址调用吗?
注意,虽然GetMemory的参数是指针,且传入的实参也是指针,但是GetMemory是对这个指针本身进行修改,并不是修改这个指针指向的内容;形参pstr的一份临时拷贝,这个函数是一个传值调用,函数内部的操作是不会对实参本身产生任何影响的;
注意不要看到函数的参数是指针就把它当成传址调用,应该结合这个指针在函数中执行什么样的操作来判断

2. 题目2

#include <stdio.h>

char* GetMemory()
{
	char p[] = "hello world";
	return p;
}

void Test()
{
	char *str = NULL;
	str = GetMemory();
	printf(str);
}

int main()
{
	Test();

	return 0;
}

分析:
上面代码的目的应该是,Test函数通过调用GetMemory函数来获得"hello world"这个字符串的地址,并在Test中输出它;
但结果真的如此吗?
并不是对吧,注意GetMemory函数中的字符数组p是一个局部变量,且没有static修饰,并不是静态变量,所以这个变量的生命周期只在GetMemory函数中,离开函数这个变量就销毁了,所以当Test函数使用这个地址时,其实它已经变成了一个野指针,这片空间指向的内容是什么完全确认;

运行结果:
在这里插入图片描述

可以看到结果和分析的相同,这个地址指向内容变成了随机值;

3. 题目3

#include <stdio.h>

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}

void Test()
{
	char *str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

int main()
{
	Test();

	return 0;
}

分析:
GetMemory函数中动态开辟了一片空间,并通过指针操作,将Test函数传给GetMemory的参数str,此时是一个传址调用,此时的代码是符合设计的;
运行结果:
在这里插入图片描述

4. 题目4

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

void Test()
{
	char *str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

int main()
{
	Test();

	return 0;
}

分析:
这就是一个典型的野指针问题,str接收了动态开辟的空间,在调用了free函数之后,str指向的空间已经被系统回收,此时这片空间已经不属于str,但是并没有手动将str指向NULL,所以if语句块中的语句会被执行,会输出"world"(虽然这片空间已经不属于str,但strcpy这个函数不会管你这些,你给出指令,它就会执行对应的操作);

运行结果:
在这里插入图片描述
在这里插入图片描述

六、C/C++程序的内存开辟

我们曾经简单的介绍了C\C++中程序的内存区域划分,分为栈区、堆区、静态区三个区域,现在我们再对这部分进行详细说明;
在这里插入图片描述
C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等;
  2. 堆区:一般由程序员分配释放,若程序员不释放,程序结束可能由OS回收。分配方式类似于链表;
  3. 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放;
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码;

从这幅图中更能理解为什么用static修饰的局部变量生命周期会变长,因为静态变量存放在数据段,数据段的特点是在上面创建的变量会一直存在,直到程序结束才销毁,所以生命周期变长。

七、柔性数组

在C99中,结构体的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。
例如:

typedef struct st_type
{
	int i;
	int a[0]; // 柔性数组成员
} type_a;

有时上面代码编译器会报错,可以改成:

typedef struct st_type
{
	int i;
	int a[]; // 柔性数组成员
} type_a;

1. 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员;
  • sizeof返回这种结构大小不包括柔性数组的内存;
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小;

例如:

#include <stdio.h>

typedef struct st_type
{
	int i;
	int a[]; // 柔性数组成员
} type_a;

int main()
{
	printf("%d\n", sizeof(type_a));

	return 0;
}

运行结果:
在这里插入图片描述

2. 柔性数组的使用

// 代码1
#include <stdio.h>
#include <stdlib.h>

typedef struct st_type
{
	int i;
	int a[]; // 柔性数组成员
} type_a;

int main()
{
	int i = 0;
	type_a *p = (type_a*)malloc(sizeof(type_a) + sizeof(int) * 100);
	if (p == NULL)
	{
		perror("malloc");
		return;
	}
	p->i = 100;
	for (i = 0; i < p->i; i++)
	{
		p->a[i] = i;
	}
	free(p);
	p = NULL;

	return 0;
}

上面的代码在堆区开辟了一片空间用于保存这个结构体的信息,这个结构体中有一个长度为100的int数组;
柔性数组a有100个int元素的连续空间;

3. 柔性数组的优势

上面的代码还有另一种写法:

// 代码2
#include <stdio.h>
#include <stdlib.h>

typedef struct
{
	int i;
	int *p_a;
} type_a;

int main()
{
	type_a *p = (type_a*)malloc(sizeof(type_a));
	if(p == NULL)
	{
		perror("malloc p");
		return ;
	}
	p->i = 100;
	p->p_a = (int*)malloc(p->i * sizeof(int));
	if(p->p_a == NULL)
	{
		perror("malloc P_a");
		return ;
	}

	int i = 0;
	for (i = 0; i < p->i; i++)
	{
		*(p->p_a + i) = i;
	}

	// 一定要按照顺序,否则会出错
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;

	return 0;
}

代码1和代码2才能实现同样的功能,但是代码1有两个好处:

  1. 方便内存释放:

    可以看到,在代码2中进行了两次内存释放操作,因为结构体内部的成员也进行了动态内存开辟;
    当这个函数是我们封装好提供给他人使用的函数,别人是不知道,我们也不希望别人知道这个函数是怎样实现的,所以用户是不知道这个结构体内部的成员在使用结束后也需要释放内存,这就会产生问题;
    使用代码1的写法,直接释放结构体指针指向的内存空间,就能够完成动态开辟的内存空间的释放;

  2. 有利于访问速度

    代码1中整个结构体是在一片连续的堆区空间中,代码2则是数组和结构体的内存空间并不连续;连续的内存有益于提高访问速度,也有益于减少内存碎片;

所以上面修改后的通讯录还可以使用柔性数组来实现;

typedef struct
{
	int age; // 年龄
	char name[20]; // 名字
	char sex[7]; // 性别
	char phone_number[20]; // 电话号码
	char address[30]; // 地址
} Peoinfo; // 保存人的信息

typedef struct
{
	int count; // 当前通讯录中的人数
	int num; // 当前通讯录中能保存的最大人数
	Peoinfo data[0]; // 指向保存人信息的空间
} Contact;

在空间不足时使用realloc函数来扩容即可;


总结

本文介绍了动态内存的开辟方法,并结合这个技术修改了之前的通讯录程序,这个技术使得程序员可以在程序执行过程中动态的调整变量占据的内存空间大小,极大的扩展了程序的功能;并在之后详细解读了动态内存开辟的6种错误;在最后介绍了柔性数组;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2329944.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL表的增删改查基础版

这一部分内容比较多&#xff0c;请大家结合目录查看&#x1f440; 增删改查 这一部分内容比较多&#xff0c;请大家结合目录查看&#x1f440; 一、新增1.插入2.指定列插入3.一次插入多行记录 二、查询1.全列查询2.指定列查询3.查询字段为表达式4.别名5.去重6.多列去重7.排序8.…

【备赛】蓝桥杯嵌入式实现led闪烁

原理 由于蓝桥杯的板子带有锁存器&#xff0c;并且与lcd屏幕有冲突&#xff0c;所以这个就成了考点。 主要就是用定时器来实现&#xff0c;同时也要兼顾lcd的冲突。 一、处理LCD函数 首先来解决与lcd屏幕冲突的问题&#xff0c;把我们所有用到的lcd函数改装一下。 以下是基…

【Python】贝叶斯,条件概率是怎么回事儿

【Python】贝叶斯&#xff0c;条件概率是怎么回事儿 一、原理简介1.1 贝叶斯定理1.2 朴素贝叶斯假设 二、算法实现过程2.1 数据准备与预处理2.2 模型训练与预测2.2.1 高斯朴素贝叶斯 - 对应连续型数据2.2.2 多项式朴素贝叶斯 - 离散型数据 2.3 模型评估 三、算法优缺点分析3.1 …

Flink介绍——实时计算核心论文之Storm论文详解

引入 我们通过以下两篇文章&#xff0c;深入探索了S4是如何抽象流式计算模型&#xff0c;如何设计架构和系统&#xff0c;存在那些局限&#xff1a; 论文详解论文总结 Yahoo推出的S4 并没有在历史舞台上站稳脚跟&#xff0c;在S4的论文发表的同一年&#xff0c;我们今天的主…

001 使用单片机实现的逻辑分析仪——吸收篇

本内容记录于韦东山老师的毕设级开源学习项目&#xff0c;含个人观点&#xff0c;请理性阅读。 个人笔记&#xff0c;没有套路&#xff0c;一步到位&#xff0c;欢迎交流&#xff01; 00单片机的逻辑分析仪与商业版FPGA的逻辑分析仪异同 对比维度自制STM32逻辑分析仪商业版逻…

11-产品经理-创建产品

在“产品”-“仪表盘”内&#xff0c;可以查看系统中关于产品及相关需求的统计。 在“产品”-“产品列表”页面&#xff0c;可以按项目集、项目查看其关联产品。还可以添加产品、编辑产品线、或者导出产品列表。 产品看板&#xff0c;通过看板方式查看产品、产品计划和产品下的…

低代码开发平台:飞帆制作网页并集成到自己的网页中

应用场景&#xff1a; 有时&#xff0c;我们的网页使用了某个模版&#xff0c;或者自己写的 html、css、javascript 代码。只是网页中的一部分使用飞帆来制作。这样的混合网页如何实现呢&#xff1f; 其实很容易&#xff0c;来体验一下飞帆提供的功能&#xff01; 还记得这个…

语法: result=log (x);

LOG( ) 语法: resultlog (x); 参数: x是一个浮点数; 返回值: result等于返回值,是一个浮点数; 功能: 该函数是用来计算浮点数x的自然对数(即ln x);如果x小于或等于0,或x太大,则行为没有定义; 注意:存在error挂起; 如果在编写程序里包含了errno.h头文件,则范围和等级…

Hibernate核心方法总结

Session中的核心方法梳理 1、save方法 这个方法表示将一个对象保存到数据库中&#xff0c;可以将一个不含OID的new出来的临时对象转换为一个处于Session缓存中具有OID的持久化对象。 需要注意的是&#xff1a;在save方法前设置OID是无效的但是也不会报错&#xff0c;在save方…

IntelliJ IDEA Maven 工具栏消失怎么办?

一、问题现象与背景 在使用 IntelliJ IDEA&#xff08;简称 IDEA&#xff09;开发 Maven 项目时&#xff0c;偶尔会遇到右侧或侧边栏的 Maven 工具栏&#xff08;显示依赖、生命周期等信息的窗口&#xff09;突然消失的情况。这可能影响开发者快速操作 Maven 构建、依赖管理等…

消息队列(kafka 与 rocketMQ)

为什么要使用消息队列?作用1: 削峰填谷(突发大请求量问题)作用2: 解耦(单一原则)作用3: 异步(减少处理时间) 如何选择消息队列(kafka&RocketMQ)成本功能性能选择 rocketMQ是参考kafka进行实现的为什么rocketMQ与kafka性能差距很大呢?kafka 的底层数据储存实现rocketMQ 的…

【STM32】Flash详解

【STM32】Flash详解 文章目录 【STM32】Flash详解1.Flash闪存概念1. 1核心区别&#xff1a;NOR Flash vs. NAND Flash1.2 为什么常说的“Flash”多指 NAND Flash&#xff1f;1.3技术细节对比(1) 存储单元结构(2) 应用场景(3) 可靠性要求 1.4总结 2.STM32内部的Flash2.1为什么是…

CV - 目标检测

物体检测 目标检测和图片分类的区别&#xff1a; 图像分类&#xff08;Image Classification&#xff09; 目的&#xff1a;图像分类的目的是识别出图像中主要物体的类别。它试图回答“图像是什么&#xff1f;”的问题。 输出&#xff1a;通常输出是一个标签或一组概率值&am…

node-modules-inspector 可视化node_modules

1、node_modules 每个vue的项目都有很多的依赖&#xff0c;有的是dev的&#xff0c;有的是生产的。 2、使用命令pnpx node-modules-inspector pnpx node-modules-inspector 3、node_modules可视化 4、在线体验 Node Modules Inspector 5、github地址 https://github.com/a…

远程服务器下载llama模型

适用于有防火墙不能直接从HF上下载的情况 然后&#xff0c;你可以克隆 Llama-3.1-8B-Instruct 模型&#xff1a; git clone https://你的用户名:你的访问令牌hf-mirror.com/meta-llama/Llama-3.1-8B-Instruct用户名&#xff0c;令牌来自huggingface官网 注意&#xff1a;要提…

2011-2019年各省地方财政金融监管支出数据

2011-2019年各省地方财政金融监管支出数据 1、时间&#xff1a;2007-2019年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;行政区划代码、地区、年份、地方财政金融监管支出 4、范围&#xff1a;31省 5、指标说明&#xff1a;地方财政在金融监管方面的支出…

Java大厂面试题 -- JVM 优化进阶之路:从原理到实战的深度剖析(2)

最近佳作推荐&#xff1a; Java大厂面试题 – 深度揭秘 JVM 优化&#xff1a;六道面试题与行业巨头实战解析&#xff08;1&#xff09;&#xff08;New&#xff09; 开源架构与人工智能的融合&#xff1a;开启技术新纪元&#xff08;New&#xff09; 开源架构的自动化测试策略优…

存储引擎 / 事务 / 索引

1. 存储引擎 MySQL 中特有的术语。 &#xff08;Oracle 有&#xff0c;但不叫这个名字&#xff09; 是一种表存储 / 组织数据的方式 不同的存储引擎&#xff0c;表存储数据的方式不同 1.1 查看存储引擎 命令&#xff1a; show engines \g&#xff08;或大写&#xff1a;G…

RabbitMQ运维

RabbitMQ运维 一.集群1.简单介绍2.集群的作用 二.搭建集群1.多机多节点搭建步骤 2.单机单节点搭建步骤 3.宕机演示 三.仲裁队列1.简单介绍2.Raft协议Raft基本概念主节点选举选举过程 3.仲裁队列的使用 四.HAProxy负载均衡1.安装HAProxy2.HAProxy的使用 一.集群 1.简单介绍 Ra…

Ansible 实战:Roles,运维的 “魔法函数”

一、介绍 你现在已经学过tasks和handlers&#xff0c;那么&#xff0c;最好的playbook组织方式是什么呢&#xff1f;答案很简单&#xff1a;使用roles&#xff01;roles基于一种已知的文件结构&#xff0c;能够自动加载特定的vars_files、tasks以及handlers。通过roles对内容进…