【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类型的空间并初始化为10int* ptr2 = new int(10);//动态申请10个int类型的空间int* ptr3 = new int[10];
}

delete的基本用法

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

void Test()
{//动态申请一个int类型的空间int* ptr1 = new int;//动态申请一个int类型的空间并初始化为10int* 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 bytesvoid* 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_FINALLYreturn;
}/*
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有所帮助!


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


    

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

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

相关文章

管理能力测评,如何提升管理能力?

管理能力是综合能力的体现&#xff0c;通常也解读为组织管理能力&#xff0c;如果要再细分的话&#xff0c;可能还包括有沟通能力&#xff0c;协调能力&#xff0c;组织能力&#xff0c;执行力和专业能力等等。不过没有办法说的太细节&#xff0c;因为每个部分铺开了都是一个独…

基于工业智能网关的汽车充电桩安全监测方案

近年来&#xff0c;我国新能源汽车产业得到快速发展&#xff0c;电动车产量和销量都在持续增长&#xff0c;不仅国内市场竞争激烈&#xff0c;而且也远销海外&#xff0c;成为新的经济增长点。但与此同时&#xff0c;充电设施的运营却面临着安全和效率的双重挑战。 当前的充电桩…

开放式耳机井喷阶段,南卡品牌何以成为“头号玩家”?

对于耳机行业而言,过去两年的聚光灯属于开放式耳机这一才闯入赛道的品类。 在消费电子寒冬持续蔓延的时间节点,夺下属于自己的高光时刻实属不易,毕竟无论是过去数年间高歌猛进的智能可穿戴设备行业,还是更垂直的耳机赛道,现阶段均面临承压的局面。 一面是“前辈”们昔日的“爆…

matlab 读写ENVI标准数据

本博客主要讲解如何读、生成ENVI标准格式的数据。主要分为四部分&#xff1a;读取ENVI头文件、读取ENVI数据、写入ENVI头文件、生成ENVI标准数据&#xff0c;最后附加讲解了本人写的生成hdr文本文件代码。此外&#xff0c;文中还具体介绍写代码的一些思路。 注&#xff1a;完整…

【C++入门】构造函数析构函数

目录 前言 1. 类的默认成员函数 2. 构造函数 2.1 什么是构造函数 2.2 构造函数的特性 3. 析构函数 3.1 什么是析构函数 3.2 析构函数的特性 前言 前边我们已经了解了类和对像的基本概念&#xff0c;今天我们将继续深入了解类。类有6个默认成员函数&#xff0c;即使类中什么都…

一款基于.Net开发、开源、支持多平台云存储文件管理器

目录 01 项目简介02 项目代码03 部分截图04 项目地址 今天给大家推荐一款基于基于.Net开发、开源的&#xff0c;支持多平台的云存储文件管理器。 01 项目简介 Camelotia是一款云存储文件管理器&#xff0c;基于.Net UI框架和ReactiveUI框架开发的&#xff0c;目前支持的平台有…

skynet学习笔记03— 服务

01、API newservice(name, ...)&#xff1a; 阻塞的形势启动一个名为 name 的新服务&#xff0c;待start函数执行完后会返回这个服务的地址。uniqueservice(name, ...)&#xff1a;针对于当前节点&#xff0c;启动一个唯一服务&#xff08;相当于单例&#xff09;&#xff0c;…

STM32F103C8T6第三天:pwm、sg90、超声波、距离感应按键开盖震动开盖蜂鸣器

1. 定时器介绍1&#xff08;317.21&#xff09; 软件定时&#xff08;之前的定时方法&#xff09;&#xff08;软件延时&#xff09;缺点&#xff1a;不精确、占用CPU资源 void Delay500ms() //11.0592MHz {unsigned char i, j, k;_nop_();i 4;j 129;k 119;do{do{while (-…

Flink SQL -- 概述

1、Flink SQL中的动态表和连续查询 1、动态表&#xff1a; 因为Flink是可以做实时的&#xff0c;数据是在不断的变化的&#xff0c;所以动态表指的是Flink中一张实时变换的表&#xff0c;表中会不断的有新的数据。但是这张表并不是真正的物理表。 2、连续查询&#xff1a; 连续…

网络安全深入学习第八课——代理与端口转发

文章目录 一、什么是代理二、正向代理三、反向代理四、正向和反向代理模拟复现 一、什么是代理 代理服务器英文全称是Proxy Server&#xff0c;其功能就是代理网络用户去取得网络信息。 形象的说&#xff1a;它是网络信息的中转站。在一般情况下&#xff0c;我们使用网络浏览…

自动驾驶学习笔记(八)——路线规划

#Apollo开发者# 学习课程的传送门如下&#xff0c;当您也准备学习自动驾驶时&#xff0c;可以和我一同前往&#xff1a; 《自动驾驶新人之旅》免费课程—> 传送门 《Apollo Beta宣讲和线下沙龙》免费报名—>传送门 文章目录 前言 路线规划 路由元素 路径搜索 最优…

P1529 [USACO2.4] 回家 Bessie Come Home 题解

文章目录 题目描述输入格式输出格式样例样例输入样例输出 提示完整代码 题目描述 现在是晚餐时间&#xff0c;而母牛们在外面分散的牧场中。 Farmer John 按响了电铃&#xff0c;所以她们开始向谷仓走去。 你的工作是要指出哪只母牛会最先到达谷仓&#xff08;在给出的测试数…

款网络拓扑自动扫描工具

Topology-Scanner是WeOps团队免费开放的一个网络拓扑自动扫描模块&#xff0c;可以自动发现网络设备的类型、网络设备之间的互联 使用方式 java -jar ./topology-scanner.jar --config_path./config/ 配置说明 1. 拓扑发现请求参数文件(request.json) ips [全网发现] 模式时…

欧拉角(横滚角、俯仰角、偏航角)、旋转矩阵、四元数的转换与解决万向节死锁

1、概述 物体的位姿&#xff08;位置和方向&#xff09;的描述方法一般使用两个坐标系来表示&#xff0c;一个是世界坐标系或地面坐标系&#xff0c;这里我都叫做地面坐标系吧&#xff0c;属于参考坐标系&#xff1b;另一个是自身的坐标系&#xff0c;以飞机为例来讲述一些常见…

selenium/webdriver运行原理与机制

最近在看一些底层的东西。driver翻译过来是驱动&#xff0c;司机的意思。如果将webdriver比做成司机&#xff0c;竟然非常恰当。 我们可以把WebDriver驱动浏览器类比成出租车司机开出租车。在开出租车时有三个角色&#xff1a; 乘客&#xff1a;他/她告诉出租车司机去哪里&a…

Redis的三种特殊数据类型

文章目录 一、Redis geospatial 地理位置二、Redis Hyperloglog 基数统计的算法三、Redis Bitmaps 位存储&#xff08;0、1&#xff09;总结 一、Redis geospatial 地理位置 1.geoadd&#xff1a;将指定的地理空间位置&#xff08;纬度、经度、名称&#xff09;添加到指定的ke…

【CocoaPods安装环境和流程以及各种情况】

CocoaPods 环境HomebrewRubyrbenvRubyGems 和 Bundler安装Ruby管理Ruby更新Ruby替换Ruby镜像方式1方式2 CocoaPods安装CocoaPodsCocoaPods使用安装的一些问题单元测试引用问题 参考的链接 环境 Homebrew $ brew --config *可以发现打印有下面一行&#xff1a; Homebrew Ruby: …

win10 下 ros + Qt 工程CMakeLists.txt

win10 下 ros Qt 工程CMakeLists.txt 系统&#xff1a;win10 ros: melodic Qt: 5.12.12 源码目录: D:\workspace\catkin_qt 示例代码 https://github.com/ncnynl/ros-qt.git 由于示例代码是Qt4 &#xff0c;目前我是用QT5,所以CMakeLists.txt 修改如下 CMakeLists.txt #####…

AI 绘画 | Stable Diffusion 进阶 Embeddings(词嵌入)、LoRa(低秩适应模型)、Hypernetwork(超网络)

前言 Stable Diffusion web ui&#xff0c;除了依靠文生图&#xff08;即靠提示词生成图片&#xff09;&#xff0c;图生图&#xff08;即靠图片提示词生成图片&#xff09;外&#xff0c;这两种方式还不能满足我们所有的绘图需求&#xff0c;于是就有了 Embeddings&#xff0…

odoo16 库存初始化 excel导入问题

最近在为一家公司实施odoo时&#xff0c;发现库存模块实施过程中按用户实际&#xff0c;产品初始化就是个问题。下面一一记录下 一个新公司&#xff0c;产品都有上百种&#xff0c;甚致几千种&#xff0c;如何把现有产品数据录入系统就是个不小的活。odoo16是有导入导出功能不…