【高并发内存池】第一篇 项目简介及定长内存池

🕺作者: 主页

我的专栏
C语言从0到1
探秘C++
数据结构从0到1
探秘Linux
菜鸟刷题集

😘欢迎关注:👍点赞🙌收藏✍️留言

🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!

文章目录

    • 1 项目简介
    • 2 笔者的独白
    • 3 一些概念
      • 3.1 什么是池化技术
      • 3.2 内存池
      • 3.3 内存池主要解决的问题
      • 3.4 malloc
    • 4 定长内存池的实现
      • 4.1 设计思路
      • 4.2 代码实现
    • 后记

1 项目简介

本项目的总目标是实现一个高并发的内存池,它的原型是google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,也就是线程缓存的malloc,实现了高效的多线程内存管理,用来替代系统的内存分配相关的函数(malloc、free)。而另一方面tcmalloc是全球大厂google开源的,并且Go语言直接用它做了自己内存分配器,所以这个项目是很值得学习的。

该项目用到的主要的知识储备有C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等等方面的知识。

2 笔者的独白

这些知识在之前写的博客里面都讲了的,如果没有讲到会在接下来的学习中讲述,毕竟我也是边写边学的hhh,我之前也没有做过这个项目,所以接下来我都是以第一视角来讲述碰到的难关、知识盲区、一些心得或者是碎碎念,因为这个项目将可能是我在今年做的最后一个稍大的项目了(现在时间2023/11/15 18:24 ),所以它对我来说也是有一种特别的“感情”的hhh,本系列不以教学为目的,纯粹是我的所思所想,所以我估计会有点“乱”?如果对你有所帮助那肯定最好了。

3 一些概念

3.1 什么是池化技术

在讲多线程的博客中实现过线程池,它的意义就在于让程序向系统申请过量的的资源自己管理,这样就不用每次都向系统申请了,大大提高程序运行效率,因为每次申请都有较大的开销,如果提前申请好,这样使用就会十分快捷,怎么理解呢?就像是小时候我向我妈要零花钱,我一会儿要一次,一次5毛,我妈就会觉得我怎么老是要,还不如一次要多点,这样就不会总是烦她,我也可以有自由发挥的空间…好了,跑题了,在计算机中有很多使用“池”这种技术的地方,除了线程池,还有连接池、内存池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。

3.2 内存池

内存池指的是程序预先从操作系统申请一块足够大的内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取,同理,释放的内存也是返回内存池。当程序退出(特定时间)时,内存池才将之前申请的内存真正释放。

3.3 内存池主要解决的问题

内存池主要解决的问题是效率问题,其次从系统的内存分配器的角度还需要解决一下内存碎片的问题。说“解决”有点夸大了,只能说是减少内存碎片的产生。
(感谢@程序猿进阶 大佬的提醒)

传统的内存分配方式和tcmalloc的方式都可能会导致内存碎片的形成,即已分配的内存单元之间存在未使用的小块内存。但是内存池的内存单元大小是固定的,并且被分配和释放的次数相对较少,相对来说可以有效减少内存碎片的产生。

什么是内存碎片?

刚好最近学了操作系统的内存碎片问题,我还研究了一阵,这里大概讲一下:
内存碎片分为内部碎片和外部碎片
外部碎片是指内存中的空间大小足够外部申请,但是因为都是一些不连续的小碎片,导致申请不成功,就像下面这样
image.png
内部碎片就是分配出去的空间中有一些内存无法被有效利用,这通常发生在分配的内存块大于进程所需的内存大小时。以下是一个例子:假设有一个操作系统,它将内存以页(Page)为单位分配给进程。每个页的大小是8KB。现在,一个进程需要分配10KB的内存。操作系统分配了2个页面(16KB)给这个进程。由于内存以页为单位分配,而进程只使用了10KB内存,还有6KB是未被使用的,这部分未被使用的内存就是内部碎片。

3.4 malloc

在C++中我们要动态申请内存一般都是通过malloc去申请内存的,但是我们要知道,实际上我们不是直接去堆获取内存的,malloc就是一个内存池,malloc相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用。当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。比如windows的vs系列用的微软自己写的一套,linux gcc用的glibc中的ptmalloc。
下面是相关的文章:
Linux内存管理及malloc、free实现原理
malloc背后的实现原理–内存池
malloc的底层实现(ptmalloc)
image.png

4 定长内存池的实现

虽然我们知道申请内存用的是malloc,显然malloc是通用的,几乎什么场景都快要使用它,但是什么场景都可以用就意味着它在什么场景下都不会有很高的性能,那么就可以自己实现一个简单的malloc热热手,它和真的malloc的区别在于我实现的是定长的,也就是说向我写的内存池里申请空间时,只能申请一个**固定空间大小的内存,**比如说只能申请1byte或2byte等等,也就是说它只能应用于一种场景,不能同时应用于多种场景。

4.1 设计思路

定长内存池首先是一个内存池,那么就需要向系统申请一批空间保存在池子中,所以需要一个指针记录池子的起始地址,还要一个变量记录长度,还有就是向定长内存池申请的空间还回来的时候不能直接还给操作系统,而是把它们单独保存起来,这样做的好处有:

  1. 减少内存分配的开销:内存分配和释放都需要一定的开销,特别是在多线程环境下,操作系统的内存分配器可能会面临锁竞争等问题,这会导致性能下降。通过将空闲的内存块保存在内存池中,可以减少频繁地与操作系统进行内存分配和释放的次数,从而提高性能。
  2. 避免内存碎片化:频繁地向操作系统申请和释放小块内存会导致内存出现碎片化,即大量不连续的小块空闲内存无法被充分利用。通过内存池管理机制,可以在一定程度上避免内存碎片化问题,因为内存池可以根据需求自行管理内存块的分配和释放。
  3. 更好地控制内存使用:内存池可以根据具体的应用场景进行优化,比如预先分配一定数量的内存块,并在需要时直接从内存池中获取,而不是每次都向操作系统请求内存。这样可以更好地控制内存的使用情况,避免出现意外的内存耗尽或内存泄漏问题。

那还回来的空间怎么保存呢?

使用链表,让链表的前几个字节记录下一个申请空间的地址即可
每次申请空间都优先在这个链表中查看是否有空间,没有再去内存池中查看是否有空间,都没有就会让内存池向堆申请空间,这样就可以减少申请空间的次数,减少开销。

4.2 代码实现

在写剩下代码前要思考一个问题 在不同的系统下申请空间的方式是不同的
下面是相关的博客介绍:
windows: VirtualAlloc
linux: brk和mmap


我们需要申请多大的空间作为内存池呢?有没有要求?

有的,在操作系统中,内存是以页为单位进行管理的。每个页的大小是固定的,通常为4KB或者8KB。当我们向操作系统申请内存时,操作系统会按照页的大小来进行分配。
如果申请的内存大小不是页大小的整数倍,操作系统可能会分配多个完整的页来满足需求,并浪费一部分内存空间。为了最大程度地利用内存并减少浪费,我们应该确保所申请的内存大小是页大小的整数倍。这里以申请128KB为例。


假设内存池的大小是128KB,代码应该怎么写(在windows环境下测试)

#ifdef _WIN32//宏定义_WIN32通常用于Windows平台的C和C++编程中,//它是一个预处理器宏,用于判断当前代码是否在Windows环境下编译#include<windows.h>
#else#endif// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage<<13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}

上述代码中,当在Windows平台下编译时,_WIN32这个宏通常由编译器自动定义,无需手动设置,从而执行#ifdef _WIN32下的代码块。而在其他平台下编译时,将执行#else下的代码块,实现了跨平台兼容性。需要注意的是,Windows平台的64位版本上编译时,通常会定义_WIN64,同时也定义了_WIN32。在Windows平台的64位版本上编译时,只有_WIN32被定义。


需要哪些变量?

在知道怎么申请空间和设计思路以后就开始写主体代码了
之前设计思路的时候讲过需要三个变量:指向内存池的指针、长度、指向自由链表的指针。

char* _memory = nullptr; //内存池指针
size_t _remainBytes = 0;  //内存块剩余空间大小
void* _firstList = nullptr; //自由链表,用来回收空间

如何实现定长?

  1. 使用非类型模板参数,使得在该内存池中申请到的对象大小是固定的N
  2. 使用模板参数,让它可以根据申请对象的的类型改变大小

综合考虑,选择使用类模板,满足多种场景(int、double、自定义…

template <class T>
class FLMemoryPool
{
public:T* New(){}void Delete(T* obj){}
private:char* _memory = nullptr; //内存池指针size_t _remainBytes = 0;  //内存块剩余空间大小void* _firstList = nullptr; //自由链表,用来回收空间};

内存池怎么管理释放的对象?

在自由链表中,因为自己存了下一块空闲空间的地址,那就可以用当前空间的前几个字节(指针大小在32位平台4位,64位平台8位)存储下一空间的地址即可,这样想要找到下一个空间就只需要解引用自身即可,怎么说呢,就是不同类型的指针解引用会访问不同大小的空间,而且在不同的机器下同样类型的指针的大小也不同,那具体选用哪个类型的呢?这是个问题 ,想想发现我们的本质需求是想解引用一个指针的大小,而指针指向数据的类型,决定了指针解引用后能向后访问的空间大小,因此我们这里需要的是一个指向指针的指针,这里使用二级指针就行了。
当我们需要访问一个内存块的前4/8个字节时,我们就可以先该内存块的地址先强转为二级指针,由于二级指针存储的是一级指针的地址,二级指针解引用能向后访问一个指针的大小,因此在32位平台下访问的就是4个字节,在64位平台下访问的就是8个字节,此时我们访问到了该内存块的前4/8个字节。
对于还回来的定长内存块,我们将它像上面所说的用链表串起来,只要让该内存块的前4或8个字节存储第一个内存块的地址,然后再让_freeList指向该内存块即可,这也是所谓的“头插”。
image.png

void Delete(T* obj)
{obj->~T();/*obj->~T();用于显式地调用类型T的析构函数来销毁对象。它会执行对象的清理逻辑,包括释放资源、删除关联对象等等。通过这个调用,我们确保对象的析构函数被正确地执行,以避免内存泄漏或其他资源相关的问题。需要注意的是,手动调用析构函数后,对象的内存仍然是有效的,但对象已经被销毁。所以在这段代码中,析构函数调用后并没有释放内存,而是将该内存块添加到自由链表(_freeList)中,以便在以后的对象分配中可以重复使用该内存块。*/*(void**)obj = _freeList;_freeList = obj;
}

怎么向内存池申请空间?

申请对象的时候,内存池应该优先把还回来的内存池对象再次重复利用,因此如果自由链表当中有内存块的话就直接从自由链表“头删”一个内存块返回即可。
image.png
如果自由链表中没有内存块,那么就在原来那大块内存中取出定长的内存块进行返回,当内存块切出后要及时更新_memory指针的指向,以及剩余空间_remainByte的值。
image.png
还要注意一点,之前说内存块的前几个字节要存一个内存块的地址,所以要保证每次申请的空间至少能够存下一个地址,所以当需要的空间小于存下一个地址所需要的空间时,就要以后者大小为准进行空间申请。
最后,当一开始申请的大块内存大小已经不足一个对象时,我们就应该使用封装的SystemAlloc函数,再次向堆申请一块内存空间,在上面这些操作中都需要及时更新_memory指针和_remainBytes值的更新。

T* New(){T* obj = nullptr;// 优先把还回来内存块对象,再次重复利用if (_freeList){void* next = *((void**)_freeList);obj = (T*)_freeList;_freeList = next;}else{// 剩余内存不够一个对象大小时,则重新开大块空间if (_remainBytes < sizeof(T)){_remainBytes = 128 * 1024;_memory = (char*)SystemAlloc(_remainBytes >> 13);if (_memory == nullptr){throw std::bad_alloc();}}obj = (T*)_memory;size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);_memory += objSize;_remainBytes -= objSize;}// 定位new,显示调用T的构造函数初始化new(obj)T;return obj;}
需要注意的是,与释放对象时需要显示调用该对象的析构函数一样,当内存块切分出来后,我们也应该使用定位new,显示调用该对象的构造函数对其进行初始化。

new(obj)T;的含义?

new(obj)T;是C++中的定位new表达式,它的作用是在指定的内存地址上调用类型T的构造函数,以便创建一个对象。
普通的new T表达式会分配一块内存,并在这块内存上调用T类的构造函数来创建对象。而定位new表达式允许我们在已经分配好的内存地址上调用构造函数,这样就可以在特定的内存区域上创建对象,而不是在默认的堆内存上分配。


定长内存池的整体代码如下:

#pragma once
#include <iostream>
#include <vector>
#include <time.h>
using std::cout;
using std::endl;#ifdef _WIN32#include<windows.h>
#else#endif// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}template <class T>
class FLMemoryPool
{
public:T* New(){T* obj = nullptr;// 优先把还回来内存块对象,再次重复利用if (_freeList){void* next = *((void**)_freeList);obj = (T*)_freeList;_freeList = next;}else{// 剩余内存不够一个对象大小时,则重新开大块空间if (_remainBytes < sizeof(T)){_remainBytes = 128 * 1024;_memory = (char*)SystemAlloc(_remainBytes >> 13);if (_memory == nullptr){throw std::bad_alloc();}}obj = (T*)_memory;size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);_memory += objSize;_remainBytes -= objSize;}// 定位new,显示调用T的构造函数初始化new(obj)T;return obj;}void Delete(T* obj){obj->~T();*(void**)obj = _freeList;_freeList = obj;}
private:char* _memory = nullptr; //内存池指针size_t _remainBytes = 0;  //内存块剩余空间大小void* _freeList = nullptr; //自由链表,用来回收空间};

性能对比

怎么测试它们的性能呢?
可以让使用new向它申请若干个TreeNode对象,然后再使用delete将这些对象释放,通过clock函数得到整个过程所消耗的时间。
然后再重复该过程,只不过将其中的new和delete替换为定长内存池当中的New和Delete,此时再通过clock函数得到该过程消耗的时间。

struct TreeNode
{int _val;TreeNode* _left;TreeNode* _right;TreeNode():_val(0), _left(nullptr), _right(nullptr){}
};void TestFLMemoryPool()
{// 申请释放的轮次const size_t Rounds = 3;// 每轮申请释放多少次const size_t N = 1000000;std::vector<TreeNode*> v1;v1.reserve(N);//malloc和freesize_t begin1 = clock();for (size_t j = 0; j < Rounds; ++j){for (int i = 0; i < N; ++i){v1.push_back(new TreeNode);}for (int i = 0; i < N; ++i){delete v1[i];}v1.clear();}size_t end1 = clock();//定长内存池FLMemoryPool<TreeNode> TNPool;std::vector<TreeNode*> v2;v2.reserve(N);size_t begin2 = clock();for (size_t j = 0; j < Rounds; ++j){for (int i = 0; i < N; ++i){v2.push_back(TNPool.New());}for (int i = 0; i < N; ++i){TNPool.Delete(v2[i]);}v2.clear();}size_t end2 = clock();cout << "new cost time:" << end1 - begin1 << endl;cout << "FLMemoryPool cost time:" << end2 - begin2 << endl;
}

结果如下,可以看到这个过程中,定长内存池消耗的时间比malloc/free少。这就是因为malloc是一个通用的内存池,而定长内存池是专门针对申请定长对象而设计的,所以这种场景下定长内存池的效率更高。
image.png

后记

最近好多考试…可能做的比较慢,加油吧

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

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

相关文章

Android 实现三维空间坐标系(WebView与JS交互,支持多条曲线,可设置坐标轴翻转等)

全部代码已经上传&#xff0c;点击上方进行下载 支持多条曲线的绘制&#xff0c;可旋转拖动放大缩小 1.布局文件&#xff1a; <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/…

plantuml最原始的主题如何设置

在startuml下一行添加 skin rose startuml skin rose:Hello world; :This is defined on several **lines**;enduml 效果如下&#xff1a; plantuml官网地址如下&#xff1a; ​​​​​​使用简单的文字描述画UML图的开源工具。轻松从简单的文字说明创建UML图。也有许多种可…

一键云端,AList 整合多网盘,轻松管理文件多元共享!

hello&#xff0c;我是小索奇&#xff0c;本篇教大家如何使用AList实现网盘挂载 可能还是有小伙伴不懂&#xff0c;所以简单介绍一下哈 AList 是一款强大的文件管理工具&#xff0c;为用户提供了将多种云存储服务和文件共享协议集成在一个平台上的便利性。它的独特之处在于&am…

Unity中Shader矩阵的转置矩阵

文章目录 前言一、转置的表示二、转置矩阵三、转置矩阵的总结1、(A^T^)^T^ A2、(A B)^T^ A^T^ B^T^3、(kA)^T^ kA^T^ (k为实数)4、(AB)^T^ B^T^A^T^5、如果 A A^T^ 则称A为对称矩阵6、如果 AA^T^ I(单位矩阵)&#xff0c;则称 A 为正交矩阵&#xff0c;同时 A^T^ A^-1…

复杂数据统计与R语言程序设计实验二

1、创建一个对象&#xff0c;并进行数据类型的转换、判别等操作&#xff0c;步骤如下。 ①使用命令清空工作空间&#xff0c;创建一个对象x&#xff0c;内含元素为序列&#xff1a;1&#xff0c;3&#xff0c;5&#xff0c;6&#xff0c;8。 ②判断对象x是否为数值型数据。 ③…

OpenGL 的学习之路-4(变换)

三大变换&#xff1a;平移、缩放、旋转&#xff08;通过这三种变换&#xff0c;可以将图像移动到任意位置&#xff09; 其实&#xff0c;这背后对应的数学在 闫令琪 图形学课程 中有过一些了解&#xff0c;所以&#xff0c;理解起来也不觉得很困难。看程序吧。 1.画三角形&am…

OpenCV入门2——图像视频的加载与展示一些API

文章目录 题目OpenCV创建显示窗口OpenCV加载显示图片题目 OpenCV保存文件利用OpenCV从摄像头采集视频从多媒体文件中读取视频帧将视频数据录制成多媒体文件OpenCV控制鼠标关于[np.uint8](https://stackoverflow.com/questions/68387192/what-is-np-uint8) OpenCV中的TrackBar控…

初试 jmeter做压力测试

一.前言 压力测试是每一个Web应用程序上线之前都需要做的一个测试&#xff0c;他可以帮助我们发现系统中的瓶颈问题&#xff0c;减少发布到生产环境后出问题的几率&#xff1b;预估系统的承载能力&#xff0c;使我们能根据其做出一些应对措施。所以压力测试是一个非常重要的步…

mac无法向移动硬盘拷贝文件怎么解决?不能读取移动硬盘文件怎么解决

有时候我们在使用mac的时候&#xff0c;会遇到一些问题&#xff0c;比如无法向移动硬盘拷贝文件或者不能读取移动硬盘文件。这些问题会给我们的工作和生活带来不便&#xff0c;所以我们需要找到原因和解决办法。本文将为你介绍mac无法向移动硬盘拷贝文件怎么回事&#xff0c;以…

CTFSHOW 文件上传

web151 JS前端绕过 直接上传 png的图片马 然后抓包修改为php asystem("ls /var/www/html"); asystem("cat /var/www/html/flag.php"); web152 和151一样的方法也可以实现上传 asystem("ls /var/www/html"); asystem("cat /var/www/html…

Anaconda的安装使用及pycharm设置conda虚拟环境

1.python和包以及anaconda的概念关系 python “工人” 包 “工具” 环境 “工具箱” anaconda “放很多工具箱的大箱子” python等于工人这个好理解&#xff0c;就是编程需要用python来实现对应功能&#xff0c;即工人完成某项工程。 包等于工具&#xff0c;就是工人…

Excel vlookup 如何使用

Excel vlookup 如何使用 打开WX, 搜索 “程序员奇点” Excel vlookup可以说是利器&#xff0c;非常好用的工具&#xff0c;用来查询 Excel 或者进行数据匹配&#xff0c;十分方便。 VLookuP 如何使用&#xff0c;不常用的同学经常容易忘记&#xff0c;这次做个记录&#xff…

【Python自动化】定时自动采集,并发送微信告警通知,全流程案例讲解!

文章目录 一、概要二、效果演示三、代码讲解3.1 爬虫采集行政处罚数据3.2 存MySQL数据库3.3 发送告警邮件&微信通知3.4 定时机制 四、总结 一、概要 您好&#xff01;我是马哥python说&#xff0c;一名10年程序猿。 我原创开发了一套定时自动化爬取方案&#xff0c;完整开…

Flutter笔记:桌面应用 窗口定制库 bitsdojo_window

Flutter笔记 桌面应用窗口管理库 bitsdojo_window 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/13446…

cesium 重点区域大屏展示效果(加载行政区划)

cesium 重点区域大屏展示效果(配色不太好看,主要看思路和方法) 1、实现思路(文张最后有**源码 **) 1、第一步将cesium背景调成透明关掉光照大气等效果相关属性都在“viewer.scene”中 2、第二步添加背景图片此背景图片直接用html加css就可以完成 3、第三步添加蒙版效果也…

【一周安全资讯1118】北京高院发布《侵犯公民个人信息犯罪审判白皮书》;工银金融勒索案的事件响应服务商MoxFive是谁?

要闻速览 1、工信部等四部门部署开展智能网联汽车准入和上路通行试点工作 2、北京高院发布《侵犯公民个人信息犯罪审判白皮书》 3、丰田公司确认遭遇美杜莎勒索软件攻击 4、家中设备把数据信息泄露到国外&#xff0c;浙江一男子被罚5000元 5、工银金融勒索案的事件响应服务商M…

计算机算法分析与设计(23)---二分搜索算法(C++)

文章目录 1. 算法介绍2. 代码编写 1. 算法介绍 1. 二分搜索&#xff08;英语&#xff1a;binary search&#xff09;&#xff0c;也称折半搜索&#xff08;英语&#xff1a;half-interval search&#xff09;、对数搜索&#xff08;英语&#xff1a;logarithmic search&#xf…

Git命令总结-常用-后续使用频繁的再添加~

Git命令总结-常用 久了不用&#xff0c;有些时候老是会忘记一些命令&#xff0c;多的都记录一下&#xff0c;方便查找 git init 初始化一个Git仓库&#xff0c;执行完git init命令后&#xff0c;会生成一个**.git**目录&#xff0c;该目录包含了资源数据&#xff0c;且只会在…

Unity Quaternion接口API的常用方法解析_unity基础开发教程

Quaternion接口的常用方法 Quaternion.Euler()Quaternion.Lerp()Quaternion.Inverse()Quaternion.RotateTowards() Quaternion在Unity中是一种非常重要的数据类型&#xff0c;用于表示3D空间中的旋转。Quaternion可以表示任何旋转&#xff0c;无论是在哪个轴上旋转多少度&#…

SQL Server如何建表

一、数据表的组成 实现完整性的约束有&#xff1a; –6个约束 –非空 not null –主键 primary key –唯一 unique –检查 check –默认 default –主键自增 identity 表约束 主键约束&#xff1a;值不能为null,且不能重复 非空约束&#xff1a;不能为null 默认约束&#xf…