【C++破局】C++内存管理之new与deleted剖析

作者主页

📚lovewold少个r博客主页

   ⚠️本文重点c++内存管理部分知识点梳理

👉【C-C++入门系列专栏】博客文章专栏传送门

😄每日一言:花有重开日,人无再少年!

目录

C/C++的内存分配机制

内存分区

1. 内核空间(Kernel Space):

2. 栈空间(Stack):

3. 内存映射段(Memory Mapping Segment):

4. 堆(Heap):

5. 数据段(Data Segment):

6. 代码段(Code Segment):

C与C++的动态内存管理方法

malloc,calloc,realloc的内存开辟函数

内存泄露  

C++的内存管理方式

new/delete操作内置类型

 new的基本用法

delete的基本用法

malloc/free和new/delete的区别

operator new与operator delete函数

重载operator new和operator delete(了解)

new和delete的底层原理

new的底层原理:

delete的底层原理:

定位new表达式

总结


前言

        在c语言的学习过程中,我们学习了c语言的动态管理内容。本章的内容主要是从C/C++的内存分配区域入手,深入浅出的讲解C++的内存分配机制和new与delete。

C/C++的内存分配机制

内存分区

1. 内核空间(Kernel Space)

   位置: 在内存的顶部,通常是最高的地址空间。
   用途:内核空间用于存放操作系统的内核代码和数据结构。这部分内存通常是受保护的,只有内核态的代码可以访问。

2. 栈空间(Stack)

   栈是向下增长的:对于栈来说,栈内存的地址是从高地址向低地址增长的。新的栈帧会被放置在当前栈顶的上方。栈帧的释放会导致栈顶向低地址移动

    位置:通常位于用户空间和内核空间之间。
    用途:栈用于存储函数的局部变量、函数参数、返回地址和函数调用的上下文信息。栈是一个后进先出(LIFO)的数据结构。
    自动分配和释放:栈内存是由系统自动分配和释放的,函数调用时分配,函数返回时释放。

3. 内存映射段(Memory Mapping Segment)

    位置:通常位于用户空间和内核空间之间。
    用途:内存映射段包括可执行文件的映射、共享库的映射以及动态链接器的映射。这些映射将磁盘上的二进制文件映射到内存中,以便执行。

4. 堆(Heap)

   堆是向上增长:堆的内存地址是由低地址向高地址增长的,新分配的内存位于已经分配的内存的上方。

   位置:通常位于内核空间和栈空间之间。
   用途:堆用于动态分配内存,程序员可以通过函数如 malloc、calloc、realloc 来在堆上分配内存。堆上的内存需要手动释放,否则可能导致内存泄漏。
   动态大小:堆的大小不是固定的,可以根据需要动态分配和释放。

5. 数据段(Data Segment)

   位置:通常位于内核空间和栈空间之间。
   用途:数据段包括已初始化的全局变量和静态变量。这些变量在程序启动时就被初始化,并在整个程序的执行周期内保持不变。

6. 代码段(Code Segment)

   位置:通常位于内核空间和栈空间之间。
   用途:代码段包含程序的机器代码,即可执行指令。这部分内存是只读的,用于存储程序的执行代码。

只读数据段中,包含了两种常量:

1. 字符串常量:
   例如,C/C++中的字符串字面值(如"Hello, World!")。
   这些字符串常量存储在只读数据段中,因为它们在程序执行期间是不可修改的。
2. 其他常量:
   例如,C/C++中的全局常量(使用 const 关键字声明的常量)。
   这些常量也通常存储在只读数据段中。


C与C++的动态内存管理方法

malloc,calloc,realloc的内存开辟函数

malloc, calloc, 和 realloc 是在C语言中用于分配内存的三个常见函数。

malloc(Memory Allocation):

示例:

void Test()
{
	int* p1 = (int*)malloc(sizeof(int));
	if (p1 == NULL)
	{
		perror("malloc");
		exit(EXIT_FAILURE);
	}
	free(p1);
}

  • 原型:void* malloc(size_t size);

  • 功能:用于动态分配指定大小的内存块。

  • 参数:size 表示要分配的字节数。

  • 返回值:如果分配成功,返回指向分配内存的指针;如果失败,返回 NULL

calloc(Contiguous Allocation):

示例:

#include<stdlib.h>
void Test()
{
	int* p1 = (int*)calloc(4,sizeof(int));
	if (p1 == NULL)
	{
		perror("calloc");
		exit(EXIT_FAILURE);
	}
	free(p1);
}
  • 原型:void *calloc(size_t num_elements, size_t element_size);

  • 功能:用于动态分配指定数量和大小的内存块,并将每个字节初始化为零。

  • 参数:num_elements表示要分配的元素数量,element_size表示每个元素的大小(字节数)。

  • 返回值:如果分配成功,返回指向分配内存的指针;如果失败,返回 NULL

realloc(Re-allocation):

示例:

#include<stdlib.h>
void Test()
{
	int* p1 = (int*)calloc(4,sizeof(int));
	if (p1 == NULL)
	{
		perror("calloc");
		exit(EXIT_FAILURE);
	}
	int* p2 = (int*)calloc(p1, sizeof(int)*10);
	if (p1 == NULL)
	{
		perror("malloc");
		exit(EXIT_FAILURE);
	}
	free(p2);
}
  • 原型:void *realloc(void *ptr, size_t new_size);

  • 功能:用于更改先前分配的内存块的大小。

  • 参数:ptr是之前由 malloc, calloc  realloc 返回的指针;new_size 是新的内存块大小。

  • 返回值:如果分配成功,返回指向重新分配内存的指针;如果失败,返回 NULL。如果 ptr  NULL,则其行为等同于 malloc(new_size)

        以上函数主要要从函数功能进行区分,要注意的是无论是哪一种函数开辟或者扩容的空间,后面都需要手动free进行释放。同时对于realloc我们扩容的空间释放只需要释放后者,他的作用是根据需要重新分配内存,可能会将现有的内存块的内容移动到新的位置,然后释放原来的内存块。如果realloc内部决定在现有块的末尾或相邻位置分配新的内存,它将返回原始块的指针,而不是分配新的块。因此,如果新的内存块被分配在了原来的内存块上,释放原来的内存块也就释放了新的内存块,因为它们实际上是同一块内存。对于用户来说,只需保留realloc返回的指针即可,而不需要显式释放原来的内存块,需要避免多次释放。

内存泄露  

        内存泄漏是指在程序运行时,动态分配的内存没有被释放,导致系统无法再次使用这些内存块。当一个程序在运行过程中分配了内存,但在不再需要这些内存时没有释放,就会发生内存泄漏。这对一个操作系统或者服务器的话影响是非常巨大的。

        内存泄漏可能导致程序运行时占用的内存不断增加,最终耗尽系统的可用内存,从而导致程序性能下降或崩溃。内存泄漏是一种常见的编程错误,因此在开发过程中应该特别注意及时释放不再需要的内存。就比如我们经常使用一个软件的时候,长时间运转我们会发现程序消耗的内存越来越大,手机或者电脑越来越卡顿,这可能就存在内存泄露的问题,内存不能及时释放,只能新开辟空间用于后续进程,反复叠加下对性能的消耗会越来越大。合理的掌握内存分配和管理,能极大的提高程序运行的效率进而提高用户使用的体验。

        因此无论我们是使用方式的什么开辟的空间,动态内存开辟的空间需要程序员自己去掌控,要及时的free开辟的空间。

C++的内存管理方式

        在C++中,由于C++向下兼容,虽然你仍然可以使用malloc和free,但是推荐使用newdelete。首先我们知道,C++是一门面向对象的语言,和C面向过程不同,我们要体现C++的优越性就一定要对底层问题的处理上进行封装调用。

        malloc很显然能完成对于内存开辟问题,但是在C++中,我们通常是构建一个自定义的类,而构建一个类后我们需要完成对类的初始化。在使用malloc的时候我们能清晰的感觉到,首先对void*的返回值我们要进行强制类型转换,并且还需要传递空间内存大小完成开辟的工作。然后我们还需要手动调用构造函数进行构造么,这也太麻烦了。

        因此对于这些地方使用极为不便和无能无力我们就需要使用新的操作方式。

new/delete

        在C++中,new和delete是用于动态内存管理操作符,它们分别用于在堆上分配和释放内存。这两个操作符提供了对动态内存的灵活控制,特别适用于需要在运行时确定内存大小的情况。

 new的基本用法

        new用于在堆上动态分配内存,并返回指向新分配内存的指针。它同时会调用对象的构造函数(如果有的话),对新分配的内存进行初始化。(一个对象就轻松被new出来咯!!!)

void Test()
{
    //动态申请一个int类型的空间
	int* ptr1 = new int;
    //动态申请一个int类型的空间并初始化为10
    int* ptr2 = new int(10);
    //动态申请10个int类型的空间
	int* ptr3 = new int[10];
}

delete的基本用法

delete用于释放通过new分配的内存。在释放内存之前,delete会调用对象的析构函数(如果有的话),对内存中的对象进行清理工作。 (一个对象的终点)

void Test()
{
    //动态申请一个int类型的空间
    int* ptr1 = new int;
    //动态申请一个int类型的空间并初始化为10
    int* ptr2 = new int(10);
    //动态申请10个int类型的空间
    int* ptr3 = new int[10];

    delete ptr1;//释放单个对象的空间
    delete ptr2;
    delete[] ptr3;//释放数组空间
}

注意:要时刻注意一一对应

  • 对于每个new,应该有一个相应的delete,对于每个new[],应该有一个相应的delete[]
  • 不要混合使用newdeletemallocfree,也不要混合使用new[]和delete[]mallocfree
  • 在使用newnew[]时,应该使用相应的deletedelete[]来释放内存,以确保正确调用析构函数。

malloc/free和new/delete的区别

        我们知道,malloc并不会去调用构造函数,free也不会去调用析构函数。对于自定义类型的空间处理上new和delete不仅能完成空间的开辟还能完成调用构造函数和析构函数。对于自定义类来讲显然new和delete更甚一筹,而对于非自定义类型的时候,两者并无太大区别,不过new和delete在使用方式上也相对更加简略了。

class A
{
public:
	A(int date = 0)
	{
		_date = date;
		printf("a()  ");
		printf("_date=%d\n", _date);
	}
	~A()
	{
		printf("~a()\n");
	}
private:
	int _date;
};
int main()
{
	printf("____________a__________________\n");
	A* a = new A;
	delete a;

	printf("____________b__________________\n");
	A* b = new A(10);
	delete b;

	printf("____________c__________________\n");
	A* c = new A[10];
	delete[] c;
	printf("____________d__________________\n");
	A* d = new A[5]{ 1,2,3,4 };
	delete[] d;
	return 0;
}

在内存管理上来讲当分配内存失败的时候两者也有区别

  • 在分配内存失败时,malloc返回 NULL,需要手动检查。
  • new运算符在分配内存失败时抛出异常,可以通过异常处理机制来处理。

operator new与operator delete函数

     在 C++ 中,new是一个关键字,它用于动态分配内存并构造对象,而operator new是与new相关的一个全局函数,用于执行实际的内存分配。delete同理。我们这里进一步剖析new和delete的底层原理。

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,
尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
 
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// try to allocate size bytes
	void* p;
	while ((p = malloc(size)) == 0)
		if (_callnewh(size) == 0)
		{
			// report no memory
			// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
 
	return (p);
}
 
 
/*
operator delete: 该函数最终是通过free来释放空间的
*/
 
void operator delete(void* pUserData)
{
	_CrtMemBlockHeader* pHead;
 
	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
 
 
	if (pUserData == NULL)
		return;
 
	_mlock(_HEAP_LOCK); /* block other threads */
	__TRY
		/* get a pointer to memory block header */
		pHead = pHdr(pUserData);
 
		/* verify block type */
		_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
 
		_free_dbg(pUserData, pHead->nBlockUse);
 
	__FINALLY
		_munlock(_HEAP_LOCK); /* release other threads */
	__END_TRY_FINALLY
	
	return;
}
 
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

        operator new与operator delete是两个全局函数,在上述代码中是其函数的实现原理,我们不难发现,其函数实际上也是通过malloc来申请空间的,成功申请后就直接返回,否则执行用户提供的空间应对不足的措施,如果用户继续申请就会抛异常。operator最终也是通过调用free来释放空间的。

        抛异常是c++中一种在程序执行过程中遇到错误或异常情况时,通过特殊的语句将程序的控制权传递给异常处理机制。这个过程通常称为"抛出异常"。异常是一种表示程序错误或不正常情况的机制。当某个异常条件发生时,程序会停止当前执行路径,然后查找能够处理这个异常的代码块。如果找到了匹配的异常处理代码块,程序会跳转到该处进行处理。如果找不到对应的处理代码,程序可能会终止运行。(后面会详细讲解)

        在main函数中,我们使用try块来包裹可能抛出异常的代码,然后使用catch块来捕获并处理异常。如果异常被抛出,控制流将跳转到匹配的catch块。

        这里我们演示一下抛异常和捕获异常:

#include <iostream>
using namespace std;

int main(void)
{
    char* p2 = nullptr;
    try 
    {
        char* p2 = new char[1024u * 1024u * 1024u * 2u - 1];
    }
    catch (const exception& e) 
    {
        cout << e.what() << endl;
    }
    printf("%p\n", p2);

    return 0;
}

        在上述演示中,我们申请了大面积的内存空间,程序并没有出现C语言的崩溃现象,而是以抛异常的方式优雅的退出并展示了错误原因。

重载operator new和operator delete(了解)

        一般情况下,不对这两个函数进行重载,但是在一些特定的情况下,我们可以完成一些特殊的请求。比如在申请和释放空间的时候有一些特殊的需求,打印一些日志信息,帮助用户检测是否存在内存泄露的情况之类的场景需求。

void* operator new(std::size_t size) 
{
    void* ptr = std::malloc(size);
    std::cout << "自定义new分配了 " << size << " 字节的内存,其地址空间为 " << ptr << std::endl;
    return ptr;
}

void operator delete(void* ptr) noexcept 
{
    std::cout << "自定义 delete:释放了地址为 " << ptr << " 的内存" << std::endl;
    std::free(ptr);
}

int main() 
{
    int* p = new int;
    delete p;

    int* arr = new int[5];
    delete[] arr;

    return 0;
}

new和delete的底层原理

new的底层原理:

  1. 计算分配的总内存大小:new首先计算要分配的总内存大小,包括对象的大小以及额外的管理信息。

  2. 调用operator new分配内存:new调用operator new函数进行实际的内存分配。可以重载它以提供自定义的分配逻辑。

  3. 调用对象的构造函数: 如果new用于分配单个对象,它会调用对象的构造函数,完成对象的初始化。

  4. 返回分配的内存地址: 最后,new返回指向新分配对象。

delete的底层原理:

  1. 调用对象的析构函数: 在使用delete释放对象之前,会调用对象的析构函数,进行对象资源的清理。

  2. 调用operator delete释放内存:delete调用operator delete函数释放之前分配的内存。可以重载它以提供自定义的释放逻辑。

  3. 返回释放的内存: 最终,操作系统可以重新利用被释放的内存。

数组的原理即是调用相应的operator new[]对多个对象的空间申请和构造,销毁同理。

定位new表达式

定位new表达式是在一个已经分配了原始的内存空间中显示的调用构造函数完成对对象的初始化操作。

new (ptr) Type(init);

其中,prt是指向已分配内存的指针,Type是对象的类型,init是可选的初始化参数(可以带参也可以不带参)。

这种形式的new主要用于在预分配的内存区域中创建对象,配合内存池使用,因为内存池分配的内存没有初始化。而不是在堆上分配新的内存。这对于一些特殊的需求,如实现自定义内存池或在固定地址处构造对象等情况非常有用。

手动调用析构函数: 当你不再需要这个对象时,需要手动调用析构函数来释放资源

int main()
{
	// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
	
	p1->~A();
	free(p1);

	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(10);
	p2->~A();//可以显示的调用析构函数,构造函数不可以。
	operator delete(p2);
	return 0;
}

总结

        本章主要从C/C++的不同内存管理机制入手,深入浅出的讲解new和delete的底层原理和一些扩展知识。对于文章中涉及到内存池和抛异常等机制我们会在后续单独讲解,本章点到为止。希望能对大家深入理解new和delete有所帮助!


    作者水平有限,如有错误欢迎指正!


    

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

向量的点积和外积

参考&#xff1a;https://www.cnblogs.com/gxcdream/p/7597865.html 一、向量的内积&#xff08;点乘&#xff09; 定义&#xff1a; 两个向量a与b的内积为 ab |a||b|cos∠(a, b)&#xff0c;特别地&#xff0c;0a a0 0&#xff1b;若a&#xff0c;b是非零向量&#xff0c;…

测量直线模组时如何降低误差?

直线模组属于高精度传动零部件&#xff0c;是机械行业中不可或缺的零部件之一&#xff0c;其具有高精度、速度快、使用寿命长等特点&#xff1b;如果直线模组的精度受损&#xff0c;则不能达到预期的使用效果&#xff0c;那么我们测量时应该如何减少误差&#xff0c;确保直线模…

【数据结构】二叉树经典例题---<你真的掌握二叉树了吗?>(第一弹)

一、已知一颗二叉树如下图&#xff0c;试求&#xff1a; (1)该二叉树前序、中序和后序遍历的结果。 (2)该二叉树是否为满二叉树&#xff1f;是否为完全二叉树&#xff1f; (3)将它转换成对应的树或森林。 (4)这颗二叉树的深度为多少? (5)试对该二叉树进行前序线索化。 (6)试对…

Vue3路由配置

目录 ​编辑 一&#xff1a;前言 二&#xff1a;配置路由 1、安装路由 2、创建各文件 1&#xff09;views 下的 index.vue 文件 2&#xff09;router 下的 index.ts 3&#xff09;App.vue 文件修改 4&#xff09;main.ts 文件修改 3、一些会遇到的报错 1&#xff09;…

【计算机毕业设计】基于微信小程序实现校园综合服务平台-芒果校园(源码+路演ppt)

项目场景&#xff1a; 这个是之前在准备比赛做的项目&#xff0c;本来拿来去参加的&#xff0c;后面因为一些原因&#xff0c;这个项目被搁置了&#xff0c;今天打开源码 好在还在&#xff0c;但当我打开的时候&#xff0c;接口发生了一些变化&#xff0c;例如 getLocation();…

Doris:多源数据目录(Multi-Catalog)

目录 1.基本概念 2.基本操作 2.1 查看 Catalog 2.2 新增 Catalog 2.3 切换 Catalog 2.4 删除 Catalog 3.元数据更新 3.1手动刷新 3.2定时刷新 3.3自动刷新 4.JDBC Catalog 4.1 上传mysql驱动包 4.2 创建mysql catalog 4.3. 读取mysql数据 1.基本概念 …

【数据结构】顺序表 | 详细讲解

在计算机中主要有两种基本的存储结构用于存放线性表&#xff1a;顺序存储结构和链式存储结构。本篇文章介绍采用顺序存储的结构实现线性表的存储。 顺序存储定义 线性表的顺序存储结构&#xff0c;指的是一段地址连续的存储单元依次存储链性表的数据元素。 线性表的&#xf…

EtherCAT报文-LRW(逻辑寻址读写)抓包分析

0.工具准备 1.EtherCAT主站 2.EtherCAT从站(本文使用步进电机驱动器) 3.Wireshark1.EtherCAT报文帧结构 EtherCAT使用标准的IEEE802.3 Ethernet帧结构,帧类型为0x88A4。EtherCAT数据包括2个字节的数据头和44-1498字节的数据。数据区由一个或多个EtherCAT子报文组成,每个子…

MYSQL操作详解

一)计算机的基本结构 但是实际上&#xff0c;更多的是这种情况: 二)MYSQL中的数据类型: 一)数值类型: 数据类型内存大小(字节)说明bit(M)M指定位数,默认为1单个二进制位值&#xff0c;或者为0或者为1&#xff0c;主要用于开/关标志tinyint1字节1个字节的整数值&#xff0c;支持…

如何将一个 HRESULT 转换为 Win32 错误码?

地球人都知道&#xff0c;可以使用 HRESULT_FROM_WIN32 这个宏将一个 Win32 错误码转换为一个 HRESULT&#xff0c;但是如何将一个 HRESULT 转换为 Win32 错误码呢&#xff1f; 让我们先看看 HRESULT_FROM_WIN32 这个宏的定义&#xff1a; #define HRESULT_FROM_WIN32(x) \ ((…

[LeetCode]-622. 设计循环队列

目录 662. 设计循环队列 题目 思路 代码 662. 设计循环队列 622. 设计循环队列 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/design-circular-queue/ 题目 设计你的循环队列实现。 循环队列是一种线性数据结构&#xff0c;其操作表现基于 FIFO&…

硝烟后的茶歇 | 中睿天下谈攻防演练之邮件攻击溯源实战分享

近日&#xff0c;由中国信息协会信息安全专业委员会、深圳市CIO协会、PCSA安全能力者联盟主办的《硝烟后的茶歇广东站》主题故事会在深圳成功召开。活动已连续举办四年四期&#xff0c;共性智慧逐步形成《年度红蓝攻防系列全景图》、《三化六防“挂图作战”》等共性研究重要成果…

NSF服务器

1.简介 1.1 NFS背景介绍 NFS是一种古老的用于在UNIX/Linux主机之间进行文件共享的协议。它古老到你必须穿着白大补才能接近一台计算机的年代。在那个年代&#xff0c;所有的联网计算机都被认为是可信的&#xff0c;而不像现今这样&#xff0c;任何人都有多种多样方法能连接到你…

视频剪辑技巧:探索画中画视频剪辑,如何制作引人入胜的视觉效果

在视频制作领域&#xff0c;画中画视频剪辑是一种备受瞩目的技术&#xff0c;它可以将多个视频画面叠加在一起&#xff0c;形成一种独特的视觉效果。这种剪辑技巧可以让观众同时看到两个或多个视频片段&#xff0c;创造出一种引人入胜的视觉体验。在开始画中画视频剪辑之前&…

SQL必知会(二)-SQL查询篇(3)-过滤数据

第4课、过滤数据 WHERE&#xff1a;过滤条件 使用 WHERE 子句 指定搜索条件进行过滤。 WHERE 子句操作符 表4-1 WHERE 子句操作符 操作符说明操作符说明等于>大于< >不等于>大于等于!不等于!>不大于<小于BETWEEN在指定的两个值之间<小于等于IS NULL为…

线程活跃性

文章目录 1. 简介2. 死锁3. 活锁4. 饥饿 1. 简介 所谓线程的活跃性&#xff0c;我们知道每个线程所要执行的java代码是有限的&#xff0c;在执行一段时间后线程自然会陷入Terminated状态&#xff0c;但由于某些外部原因导致线程一直执行不完&#xff0c;一直处于活跃状态&…

leetCode 493 翻转对 归并分治 + 图解

493. 翻转对 - 力扣&#xff08;LeetCode&#xff09; 给定一个数组 nums &#xff0c;如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对。你需要返回给定数组中的重要翻转对的数量。 求"小和"问题是&#xff0c;当我 j 来到一个位置的…

快速排序实现方法(剑指offer思路)

快速排序思想 从参与排序的数组中&#xff0c;选择一个数&#xff0c;把小于这个数的放在左边&#xff0c;大于这个数的放在右边&#xff0c;然后递归操作。 实现算法思路 选择最后一个当作参考值&#xff0c;使用small索引当作比这个数小的下标值遍历数组&#xff0c;如果小…

【Python】 Python 使用 Pillow 处理图像:几何变换

Python 使用 Pillow 处理图像&#xff1a;几何变换 pillow库操作切片、旋转、滤镜、输出文字、调色板等功能一应俱全。 1. 几何变换 Image 包含调整图像大小 resize() 和旋转 rotate() 的方法。前者采用元组给出新的大小&#xff0c;后者采用逆时针方向的角度。 调整大小并…

n-gram语言模型——文本生成源码

n-gram语言模型——文本生成源码 n-gram模型的基本原理 文本生成的步骤 1. 准备和分词 2. 构建n-gram模型 3. 平滑技术的应用 4. 生成文本 源码 在自然语言处理的领域中&#xff0c;n-gram语言模型是一种基础而强大的工具。它通过考虑词汇的序列来预测文本内容&#xff…