进阶C语言-动态内存管理

news2024/11/28 8:22:31

动态内存管理

  • 🎈1.为什么存在动态内存分配
  • 🎈2.动态内存函数的介绍
    • 🔭2.1malloc和free函数
    • 🔭2.2calloc函数
    • 🔭2.3realloc函数
  • 🎈3.常见的动态内存错误
    • 🔭3.1对NULL指针的解引用操作
    • 🔭3.2对动态开辟空间的越界访问
    • 🔭3.3对非动态开辟空间内存使用free释放
    • 🔭3.4使用free释放一块动态开辟内存的一部分
    • 🔭3.5对同一块动态内存多次释放
    • 🔭3.6动态开辟内存忘记释放(内存泄漏)
  • 🎈4.几个经典的笔试题
    • 🔭4.1题目一
    • 🔭4.2题目二
    • 🔭4.3题目三
    • 🔭4.4题目四
  • 🎈5.C/C++程序的内存开辟
  • 🎈6.使用动态内存相关的知识改进通讯录

🎈1.为什么存在动态内存分配

✅截止目前,我们掌握的内存开辟的方式有:

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

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

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

🔎但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道那数组的编译时开辟空间的方式就不能满足了。这个时候,我们就只能试试动态存开辟!

🎈2.动态内存函数的介绍

🔭2.1malloc和free函数

🏆C语言提供了一个动态开辟内存的函数:

void *malloc(size_t size);

✅这个函数向内存申请一块连续可用的空间,并返回这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者来决定。
  • 如果参数size0malloc的行为是标准是未定义的,取决于编译器。

在这里插入图片描述

int main()
{
	//申请一块空间,用来存放10个整型
	int* p = (int*)malloc(10 * sizeof(int));
	return 0;
}

✅内存的存储:
在这里插入图片描述

📖注意: mallocfree都声明在stdlib.h的头文件中。

✅运行示例:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请一块空间,用来存放10个整型
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

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

在这里插入图片描述
🏆C语言提供了另外一个函数free,专门用来做动态内存的释放和回收:

void free(void *ptr);

free函数用来释放动态开辟的内存。
在这里插入图片描述

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

🌞malloc函数申请的空间,是怎么释放的呢?

  1. free释放-主动释放
  2. 程序退出后,malloc申请的空间,也会被操作系统回收。-被动回收
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;

🔭2.2calloc函数

在这里插入图片描述
🔎malloccalloc函数除了参数的区别,calloc函数申请好空间后,会将空间初始化0,但是malloc函数不会初始化。

🔭2.3realloc函数

在这里插入图片描述
✅该函数用于对我们已开辟动态空间大小的调整!

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请一块空间,用来存放10个整型
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

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

	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}

	//空间不够,希望调整空间为20个整型的空间
	realloc(p, 20 * sizeof(int));

	//释放
	free(p);
	p = NULL;
	return 0;
}

🔎但是这种方法是有问题的,因为realloc开辟空间也可能会失败,失败的时候返回NULL!因此,如果这个时候,我们还用p来接收的话,那么我们原来有的10个字节的空间可能也丢失了。

//更改:
//空间不够,希望调整空间为20个整型的空间
	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}

realloc函数是如何工作的呢?

  1. 要扩展的内存就直接在原有内存之后追加空间,原来空间的数据不发生变化。
  2. 原有空间之后没有足够多的空间,扩展的方法就是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存空间。

🎈3.常见的动态内存错误

🔭3.1对NULL指针的解引用操作

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

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

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

🔭3.3对非动态开辟空间内存使用free释放

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

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

void test()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

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

void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}

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

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

❗**注:**动态开辟的空间一定要释放,并且正确释放。

🎈4.几个经典的笔试题

🔭4.1题目一

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

🏆请问运行Test函数会有什么样的结果?

//当程序对NULL的进行解引用操作的时候,程序崩溃!后序代码也不会执行。
strcpy(str, "hello world");

//同时malloc开辟的空间没有释放,内存会泄露!
p = (char*)malloc(100);
//更正:
#include <stdio.h>
#include <stdlib.h>
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();
	return 0;
}

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

//更正二:
#include <stdio.h>
#include <stdlib.h>
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;
}

在这里插入图片描述

🔭4.2题目二

#include <stdio.h>
#include <stdlib.h>
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

🏆请问运行Test函数会有什么样的结果?

str = GetMemory();//返回栈空间地址的问题,野指针

✅可以这样进行修改:

🔭4.3题目三

#include <stdio.h>
#include <stdlib.h>
void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}
int main()
{
	Test();
	return 0;
}

🏆请问运行Test函数会有什么样的结果?

*p = (char*)malloc(num);//malloc申请的空间没有释放
//更正:
#include <stdio.h>
#include <stdlib.h>
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(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

在这里插入图片描述

🔭4.4题目四

#include <stdio.h>
#include <stdlib.h>
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

🏆请问运行Test函数会有什么样的结果?

strcpy(str, "world");//非法访问内存的情况,str为野指针		

🎈5.C/C++程序的内存开辟

在这里插入图片描述
C/C++程序内存分配的几个区域:

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

🎈6.使用动态内存相关的知识改进通讯录

  1. 通讯录刚开始时,可以存放3个人的信息。
  2. 空间如果放满,每次可以增加2个信息的空间。
contact.h
#pragma once
//类型的声明
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#define Max 100
#define NAME_MAX 10
#define DEFAULT_SZ 3
typedef struct PepInfo
{
	char name[NAME_MAX];
	int age;
	char sex[5];
	char tele[12];
	char addr[20];
}PInfo;

//静态通讯录
//typedef struct Contact
//{
//	PInfo data[Max];
//	int sz;//用于记录当前通讯录中存放了多少个人的信息
//}Contact;

//动态通讯录的版本
typedef struct Contact
{
	PInfo* data;//存放数据
	int sz;//记录当前通讯录中存放的人的信息的个数
	int capacity;//记录通讯录的容量
}Contact;

//初始化通讯录
void InitContact(Contact* c);

//增加联系人
void AddContact(Contact* c);

//删除指定的联系人
void DelContact(Contact* c);

//查找指定的联系人
void SearchContact(Contact* c);

//修改指定联系人
void ModifyContact(Contact* c);

//按照年龄排序
void AgeSortContact(Contact* c);

//销毁通讯录
void DestroyContact(Contact *c);

contact.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"
//静态的版本
//void InitContact(Contact *c)
//{
//	assert(c);
//	c->sz = 0;
//	memset(c->data, 0, sizeof(c->data));
//}

void InitContact(Contact* c)
{
	assert(c);
	c->sz = 0;
	c->capacity = DEFAULT_SZ;
	c->data = calloc(c->capacity, sizeof(PInfo));
	if (c->data == NULL)
	{
		perror("InitContact->calloc");
		return;
	}
}

//增容的函数可以单独封装
void CheckCapacity(Contact* c)
{
	if (c->sz == c->capacity)
	{
		PInfo* ptr = (PInfo*)realloc(c->data, (c->capacity + 2) * sizeof(PInfo));
		if (ptr != NULL)
		{
			c->data = ptr;
			c->capacity += 2;
			printf("增容成功!\n");
		}
		else
		{
			perror("AddContact->realloc");
			return;
		}
	}
}

//销毁通讯录
void DestroyContact(Contact* c)
{
	free(c->data);
	c->data = NULL;
	c->sz = 0;
	c->capacity = 0;
}

void AddContact(Contact* c)
{
	//首先要判断该通讯录是否已经满了
	assert(c);
	//增加容量
	CheckCapacity(c);
	if (c->sz == Max)
	{
		printf("通讯录已满,无法增加!\n");
		return;
	}
	printf("请输入姓名:");
	scanf("%s", c->data[c->sz].name);
	printf("请输入年龄:");
	scanf("%d", &c->data[c->sz].age);
	printf("请输入性别:");
	scanf("%s", c->data[c->sz].sex);
	printf("请输入电话:");
	scanf("%s", c->data[c->sz].tele);
	printf("请输入地址:");
	scanf("%s", c->data[c->sz].addr);
	c->sz++;
	printf("增加成功!\n");
}

void ShowContact(const Contact* c)
{
	assert(c);
	if (c->sz == 0)
	{
		printf("通讯录为空,无需打印!\n");
	}
	int i = 0;
	printf("%-20s%-5s%-5s%-12s%-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	for (int i = 0; i < c->sz; i++)
	{
		printf("%-20s%-5d%-5s%-12s%-30s\n",
			c->data[i].name, c->data[i].age, c->data[i].sex, c->data[i].tele, c->data[i].addr);
	}
}

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

void DelContact(Contact* c)
{
	char name[NAME_MAX];
	assert(c);
	if (c->sz == 0)
	{
		printf("通讯录为空,无法删除!\n");
		return;
	}
	printf("输入要删除人的姓名:");
	scanf("%s", name);
	//找到姓名为name的人
	int ret = FindByName(c, name);
	if (ret == -1)
	{
		printf("要删除的人不存在!\n");
		return;
	}
	//删除这个人
	int i = 0;
	for (i = ret; i < c->sz - 1; i++)
	{
		c->data[i] = c->data[i + 1];
	}
	c->sz--;
	printf("删除成功!\n");
}

void SearchContact(Contact* c)
{
	char name[NAME_MAX];
	assert(c);
	printf("请输入要查找人的姓名:");
	scanf("%s", name);
	int ret = FindByName(c, name);
	if (ret == -1)
	{
		printf("要查找的人不存在!\n");
		return;
	}
	//若找到了,打印出相关信息
	printf("%-20s%-5s%-5s%-12s%-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	printf("%-20s%-5d%-5s%-12s%-30s\n",
		c->data[ret].name, c->data[ret].age, c->data[ret].sex, c->data[ret].tele, c->data[ret].addr);
}

void ModifyContact(Contact* c)
{
	char name[NAME_MAX];
	assert(c);
	printf("请输入要修改人的姓名:");
	scanf("%s", name);
	int ret = FindByName(c, name);
	if (ret == -1)
	{
		printf("要修改的人不存在!\n");
		return;
	}
	//修改
	printf("请输入姓名:");
	scanf("%s", c->data[ret].name);
	printf("请输入年龄:");
	scanf("%d", &c->data[ret].age);
	printf("请输入性别:");
	scanf("%s", c->data[ret].sex);
	printf("请输入电话:");
	scanf("%s", c->data[ret].tele);
	printf("请输入地址:");
	scanf("%s", c->data[ret].addr);
	printf("修改成功!\n");
}

int cmp(const void *a,const void *b)
{
	return strcmp((*(PInfo*)a).age, (*(PInfo*)b).age);
}

void AgeSortContact(Contact* c)
{
	assert(c);
	qsort(c->data, c->sz, sizeof(PInfo), cmp);
	printf("%-20s%-5s%-5s%-12s%-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	for (int i = 0; i < c->sz; i++)
	{
		printf("%-20s%-5d%-5s%-12s%-30s\n",
			c->data[i].name, c->data[i].age, c->data[i].sex, c->data[i].tele, c->data[i].addr);
	}
}

test.c
//文件用于测试通讯录的基本功能。
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"//自己定义的头文件用""
void menu()
{
	printf("***********************************\n");
	printf("********1.增加联系人***************\n");
	printf("                                   \n");
	printf("********2.删除指定联系人的信息*****\n");
	printf("                                   \n");
	printf("********3.查找指定联系人的信息*****\n");
	printf("                                   \n");
	printf("********4.修改指定联系人的信息*****\n");
	printf("                                   \n");
	printf("********5.排序通讯录的信息*********\n");
	printf("                                   \n");
	printf("********6.显示所有联系人的信息*****\n");
	printf("                                   \n");
	printf("********0.退出程序*****************\n");
	printf("***********************************\n");
}
enum Option
{
	EXIT,
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SHOW,
	SORT
};
int main()
{
	int input = 0;
	Contact con;
	//初始化函数
	InitContact(&con);
	do
	{
		menu();
		printf("请输入你的选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case ADD:
			AddContact(&con);
			system("pause");
			system("cls");
			break;
		case DEL:
			DelContact(&con);
			system("pause");
			system("cls");
			break;
		case SEARCH:
			SearchContact(&con);
			system("pause");
			system("cls");
			break;
		case MODIFY:
			ModifyContact(&con);
			system("pause");
			system("cls");
			break;
		case SHOW:
			ShowContact(&con);
			system("pause");
			system("cls");
			break;
		case SORT:
			AgeSortContact(&con);
			system("pause");
			system("cls");
			break;
		case EXIT:
			DestroyContact(&con);
			printf("退出通讯录\n");
			break;
		default:
			break;
		}
	} while (input);
	return 0;
}

好啦,关于动态内存管理的知识到这里就先结束啦,后期会继续更新学习C语言的相关知识,欢迎大家持续关注、点赞和评论!❤️❤️❤️

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

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

相关文章

2024.2.9

作业1 请使用递归实现n&#xff01; #include<stdio.h> #include<string.h> #include<stdlib.h>int fun(int m) {if(m0)return 1;else{return m*fun(m-1);} } int main(int argc, const char *argv[]) {int m;printf("please enter m:");scanf(&…

TCP高频知识点

本篇文章主要讲述一下在面试过程中TCP的高频知识点 1.TCP三次握手流程图: 客户端发送一个SYN&#xff08;同步&#xff09;报文段给服务器&#xff0c;选择一个初始序列号&#xff0c;并设置SYN标志位为1。服务器接收到客户端的SYN报文段后&#xff0c;回复一个ACK&#xff08…

Linux_进程概念

硬件系统 软件系统 进程概念 进程状态 孤儿进程 进程优先级 一.硬件系统 1.1 冯诺依曼体系结构 数学家冯诺依曼提出了计算机制造的三个基本原则&#xff0c;即采用二进制逻辑、程序存储执行以及计算机由五个部分组成&#xff08;运算器、控制器、存储器、输入设备、输出设备&a…

MATLAB实现朴素贝叶斯分类

朴素贝叶斯(Naive Bayes)是一种基于贝叶斯定理的分类算法,它假设特征之间相互独立,从而简化了计算复杂性。该算法常用于文本分类、垃圾邮件过滤、情感分析等应用场景。 MATLAB实现鸢尾花数据集分类代码如下: clear load fisheriris X = meas(:,1:2); Y = species; labels…

勒索病毒最新变种.target勒索病毒来袭,如何恢复受感染的数据?

导言&#xff1a; 在当今数字化时代&#xff0c;数据被视为企业和个人最重要的资产之一。然而&#xff0c;随着技术的进步&#xff0c;网络安全威胁也在不断演变。其中&#xff0c;勒索病毒是一种极具破坏性的威胁&#xff0c;而".target"勒索病毒是近期备受关注的一…

【AI视野·今日CV 计算机视觉论文速览 第293期】Fri, 19 Jan 2024

AI视野今日CS.CV 计算机视觉论文速览 Fri, 19 Jan 2024 Totally 103 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computer Vision Papers ParaHome: Parameterizing Everyday Home Activities Towards 3D Generative Modeling of Human-Object Interactions Aut…

NSSCTF Round#18 RE GenshinWishSimulator WP

恶搞原神抽卡模拟器 看到软件的界面&#xff0c;大致有三种思路&#xff1a; 修改石头数量一直抽&#xff0c;如果概率正常肯定能抽到&#xff08;但是估计设置的概率是0&#xff09;在源码里找flag的数据把抽卡概率改成100%直接抽出来 Unity逆向&#xff0c;根据经验应该dnsp…

软考26-上午题-图3

一、图的遍历 从图中的某个顶点出发&#xff0c;沿着某条搜索路径对图中的所有顶点进行访问&#xff0c;且&#xff0c;只访问一次的过程。 图的遍历比树的遍历复杂&#xff0c;因为要避免对顶点进行重复访问&#xff0c;所以在图的遍历过程中&#xff0c;必须记下每个已访问…

文献速递:肿瘤分割---- 弱监督肝肿瘤分割,使用Couinaud区段标注

文献速递&#xff1a;肿瘤分割---- 弱监督肝肿瘤分割&#xff0c;使用Couinaud区段标注 01 文献速递介绍 肝癌是世界上导致癌症死亡的主要原因之一&#xff0c;也是第二大常见的癌症死因。本稿件于2021年10月28日收到&#xff0c;2021年11月24日修订&#xff0c;2021年12月1…

详解Vue文件结构+实现一个简单案例

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

thinkphp6入门(20)-- 如何上传图片、文件

1. 配置文件 设置上传的路径 对应文件夹 2. 前端 <div class"card-body"><h1 class"card-title">用户头像</h1><img src"../../../uploads/{$user.avatar_photo_path}" alt"avatar" height"100"/&g…

ICLR 2024 | Harvard FairSeg:第一个研究分割算法公平性的大型医疗分割数据集

近年来&#xff0c;人工智能模型的公平性问题受到了越来越多的关注&#xff0c;尤其是在医学领域&#xff0c;因为医学模型的公平性对人们的健康和生命至关重要。高质量的医学公平性数据集对促进公平学习研究非常必要。现有的医学公平性数据集都是针对分类任务的&#xff0c;而…

【开源】JAVA+Vue.js实现海南旅游景点推荐系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用户端2.2 管理员端 三、系统展示四、核心代码4.1 随机景点推荐4.2 景点评价4.3 协同推荐算法4.4 网站登录4.5 查询景点美食 五、免责说明 一、摘要 1.1 项目介绍 基于VueSpringBootMySQL的海南旅游推荐系统&#xff…

CUDA编程 - 共享内存 - shared memory - 学习记录

CUDA编程 - 共享内存 共享内存一、为什么要使用 shared memory&#xff1f;1.1、从硬件出发理解&#xff1a;1.2、从软件出发理解&#xff1a; 二、如何使用shared memory2.1、静态共享内存2.2、动态共享内存 三、实践 - 使用共享内存执行矩阵乘法总结 共享内存 一、为什么要使…

项目02《游戏-04-开发》Unity3D

基于 项目02《游戏-03-开发》Unity3D &#xff0c; 因前三集资源以及代码冗余问题&#xff0c;本次项目对前三集进行了重做&#xff0c;资源及代码如下&#xff0c; 首先导入场景及人物资源&#xff0c; 为人物添加动画控制器Animator组件&#xff0c; 创建动画控…

幻兽帕鲁游戏官方更新了版本,联机时提示版本不适用,无法加入,怎么办?

如果你在登录游戏的时候提示&#xff1a;您正在尝试加入的比赛正在运行不兼容的游戏版本。请尝试升级游戏版本。此时就说明你需要更新部署在服务器内的幻兽帕鲁了。 1、如果你使用幻兽帕鲁应用模板部署游戏&#xff0c;那么可以选择使用游戏配置面板一键更新。 2、如果你使用一…

《Git 简易速速上手小册》第3章:分支管理(2024 最新版)

文章目录 3.1 创建与合并分支3.1.1 基础知识讲解3.1.2 重点案例&#xff1a;为 Python 项目添加新功能3.1.3 拓展案例 1&#xff1a;使用 Pull Requests (PRs) 在团队中合作3.1.4 拓展案例 2&#xff1a;解决合并冲突 3.2 分支策略的最佳实践3.2.1 基础知识讲解3.2.2 重点案例&…

如何使用 sqlalchemy declarative base 多层次继承

在SQLAlchemy中&#xff0c;通过declarative_base创建的基类可以通过多层次的继承建立继承关系。这允许你在数据库中创建具有继承结构的表。在我使用某数据库做中转的时候&#xff0c;经常会遇到各种各样的问题&#xff0c;例如下面的问题&#xff0c;通过记录并附上完美的解决…

【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践

&#x1f389;&#x1f389;欢迎光临&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;特别推荐给大家我的最新专栏《Spring 狂野之旅&#xff1a;底层原理高级进阶》 &#x1f680…

系统架构25 - 软件架构设计(4)

软件架构复用 软件产品线定义分类原因复用对象及形式基本过程 软件产品线 软件产品线是指一组软件密集型系统&#xff0c;它们共享一个公共的、可管理的特性集&#xff0c;满足某个特定市场或任务的具体需要&#xff0c;是以规定的方式用公共的核心资产集成开发出来的。即围绕…