C++11 解决内存泄露问题的智能指针:shared_ptr、unique_ptr、weak_ptr

news2024/10/6 16:26:00

        我们经常听到内存泄漏,但是对这个抽象的概念一直没有什么理解,比如产生内存泄漏又将如何,我平时写程序从来不考虑这个等等。这篇的目的:第一,给大家实验实验内存泄露带来的问题,让大家直观感受内存泄露。第二,介绍C++11新特性智能指针shared_ptr、unique_ptr、weak_ptr。

目录

一、内存泄露

二、智能指针 

2.1 shared_ptr

2.1.1 实现原理及代码

2.1.2 shared_ptr的使用

2.2 unique_ptr

2.2.1 unique_ptr原理及代码实现

2.2.2 unique_ptr使用方式

2.3 weak_ptr

2.3.1 原理及简单实现

2.3.2 weak_ptr的作用何在?

2.3.3 具体使用方法

三、总结


一、内存泄露

        申请的内存未得到释放而引发的内存泄露指的是,程序在使用指针动态分配内存时,由于程序设计问题,没有释放已经不再使用的内存,造成程序的内存使用量逐渐增加。这会带来以下结果:

        1. 如果内存泄露这个过程累积会占用大量内存,直至内存不足程序崩溃、系统假死。

        2. 系统的性能会受到影响,系统会自动回收这些内存从而导致系统不流畅了。

        3. 占用内存是实际的物理内存,会影响其他程序的运行,因为虚拟内存映射到物理内存是需要实际物理内存的,如果物理内存被占太多,就会导致其他程序无内存可用。

        一个具体的例子:

        下面是一段 C 语言代码,它用于动态分配内存来存储一个字符串:

char* myString = (char*) malloc(100);
strcpy(myString, "Hello, World!");
printf("%s\n", myString);

        如果我们不释放 myString 指向的内存,那么这段代码就会导致内存泄露。为了避免内存泄露,我们应该在不再使用 myString 指向的内存后,调用 free 函数释放这段内存。

char* myString = (char*) malloc(100);
strcpy(myString, "Hello, World!");
printf("%s\n", myString);
free(myString);

        这样就能避免内存泄露了。

        问题:内存泄露在程序执行完后的内存会得到释放吗?

        回答:内存泄露在程序执行完后的内存并不会得到释放。当程序结束时,系统会释放程序所占用的所有资源,包括内存,但是这些内存中包含的未释放的空间并不会得到释放,而是被系统回收。

        如果程序存在大量的内存泄露,那么系统在程序结束时就需要释放大量的内存,这会导致系统性能下降。而且这些内存可能被其它程序或系统进程重新利用,这样会占用一些系统的资源,导致系统性能下降。

        为了避免这种情况的发生,应该尽早地发现和修复程序中的内存泄露问题,让程序能够及时释放不再使用的内存。

        问题:内存泄露的问题很严重吗,可以避免吗?有什么方法?

        回答:内存泄露问题是一种常见的程序设计问题,在大型程序中特别容易发生。如果程序存在大量的内存泄露,那么它可能导致系统性能下降,甚至导致程序崩溃或系统假死。

        

尽管内存泄露问题是一种常见问题,但是可以通过一些方法来避免它:

  • 明确程序的内存使用计划,确保程序及时释放不再使用的内存。

  • 使用自动内存管理工具,如 C++ 的智能指针,来管理内存。

  • 使用内存检测工具,如 valgrind,在程序运行时自动检测内存泄露。

  • 在代码审查和测试过程中特别关注内存管理问题。

        这四条建议貌似只有第二条是有实际作用的,其他的就像喊口号一样食之无味,弃之可惜。

二、智能指针 

        智能指针实现的原理很简单,通过使用对象去管理资源,并使用引用计数规则来进行统计对象使用次数,当使用次数减小至0时自动调用析构释放之前分配的内存,从而避免内存泄露。

        我们来看一个简单的示例:

#include <iostream>
#include <memory>

struct BigObj {
    BigObj() {
        std::cout << "big object has been constructed" << std::endl;
    }
    ~BigObj() {
        std::cout << "big object has been destructed" << std::endl;
    }
};

void test_shared_ptr() {
    BigObj *p = new BigObj();
    std::shared_ptr<BigObj> sp(p);

    std::shared_ptr<BigObj> sp1(new BigObj());
    std::shared_ptr<BigObj> sp2 = std::make_shared<BigObj>();
}

std::shared_ptr<BigObj> get_obj() {
    return std::make_shared<BigObj>();
}

int main() {

    test_shared_ptr();
    auto p = get_obj();

}

输出:

big object has been constructed
big object has been constructed
big object has been constructed
big object has been destructed
big object has been destructed
big object has been destructed
big object has been constructed
big object has been destructed

        可见,我们动态创建的4个BigObj指针对象都被释放了。

        智能指针一共分为三种:shared_ptr、unique_ptr、weak_ptr。

2.1 shared_ptr

2.1.1 实现原理及代码

        C++引入RAII特性,RAII可以保证任何情况下,使用对象先构造后析构。shared_ptr使用了计数器来统计对象被引用的次数,可表示shared_ptr思想的代码如下:

template<typename T>
class shared_ptr {
public:
    shared_ptr(T* p = nullptr) : ptr(p), count(new int(1)) {}
    ~shared_ptr() {
        if (--*count == 0) {
            delete ptr;
            delete count;
        }
    }
    shared_ptr(const shared_ptr& other) : ptr(other.ptr), count(other.count) {
        ++*count;
    }
    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            if (--*count == 0) {
                delete ptr;
                delete count;
            }
            ptr = other.ptr;
            count = other.count;
            ++*count;
        }
        return *this;
    }
    T& operator*() { return *ptr; }
    T* operator->() { return ptr; }

private:
    T* ptr;
    int* count;
};

        每个 shared_ptr 对象都维护了一个指向该指针引用计数的指针,当拷贝一个 shared_ptr 对象时,引用计数就会加一;每当销毁一个 shared_ptr 对象时,引用计数就会减一。当引用计数为0时,就会自动释放动态分配的内存,避免内存泄露。

 shared_ptr(const shared_ptr& other) : ptr(other.ptr), count(other.count) {
        ++*count;
    }

        上面的代码 shared_ptr的拷贝构造,当一个 shared_ptr 对象被拷贝时,它会将指针和引用计数指针指向原来的 shared_ptr 对象。这样,原来的 shared_ptr 对象和新拷贝的 shared_ptr 对象都指向了同一个对象,并且共享了同一个引用计数。这样,就可以保证在所有 shared_ptr共享一份计数。

    ~shared_ptr() {
        if (--*count == 0) {
            delete ptr;
            delete count;
        }
    }

        当 shared_ptr 对象被销毁时,它会先减少引用计数,如果引用计数变为0,则表示没有其它 shared_ptr 对象指向该对象了,那么就可以释放动态分配的内存,并释放引用计数所占用的内存。

    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            if (--*count == 0) {
                delete ptr;
                delete count;
            }
            ptr = other.ptr;
            count = other.count;
            ++*count;
        }
        return *this;
    }

        这段代码实现了 shared_ptr 的赋值操作符重载。它首先判断左操作数和右操作数是否相同,如果不同则将左操作数的引用计数减一,如果引用计数为0,就释放内存。然后将左操作数的指针和引用计数指针指向右操作数,并将右操作数的引用计数加1。这样,左操作数和右操作数就共享了同一个指针和引用计数,这就是 shared_ptr 的赋值操作的基本思路。

2.1.2 shared_ptr的使用

        

        声明shared_ptr指针的方式:

std::shared_ptr<MyClass> ptr1(new MyClass);
//or
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>("Hello", 3.14);

        具体例子: 

#include <iostream>
#include <memory>

class MyClass {
 public:
  MyClass() { std::cout << "MyClass constructed." << std::endl; }
  ~MyClass() { std::cout << "MyClass destructed." << std::endl; }
  void DoSomething() { std::cout << "MyClass is doing something." << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> ptr1(new MyClass);
    ptr1->DoSomething();

    std::shared_ptr<MyClass> ptr2 = ptr1;
    ptr2->DoSomething();

    ptr1.reset();
    ptr2.reset();
    return 0;
}

        定义了一个 MyClass 类,并在 main 函数中使用 shared_ptr 来管理该类的对象。首先,在 main 函数中,使用 std::shared_ptr<MyClass> ptr1(new MyClass); 创建了一个 shared_ptr 对象 ptr1,并使用 new 关键字动态分配了一个 MyClass 对象。然后,使用 ptr1->DoSomething(); 调用了 MyClass 的一个成员函数。

        接着,使用 std::shared_ptr<MyClass> ptr2 = ptr1; 将 ptr1 的指针和引用计数拷贝给 ptr2,此时 ptr1 和 ptr2 共享了同一个 MyClass 对象。

        最后,在程序结束前,使用 ptr1.reset(); 和 ptr2.reset(); 来释放动态分配的 MyClass 对象,并销毁 ptr1 和 ptr2。

        运行结果: 

MyClass constructed.
MyClass is doing something.
MyClass is doing something.
MyClass destructed.

2.2 unique_ptr

2.2.1 unique_ptr原理及代码实现

        unique_ptr思想的实现:

template<typename T>
class unique_ptr {
 public:
  explicit unique_ptr(T* ptr = nullptr) : ptr_(ptr) {}
  ~unique_ptr() { delete ptr_; }

  T* release() {
    T* tmp = ptr_;
    ptr_ = nullptr;
    return tmp;
  }

  T& operator*() const { return *ptr_; }
  T* operator->() const { return ptr_; }
  T* get() const { return ptr_; }

 private:
  T* ptr_;
};

        unique_ptr 是一种独占所有权的智能指针,它只能有一个指针指向一个特定的对象。

        unique_ptr 的构造函数接受一个指向 T 类型对象的指针,并将其赋值给类的成员变量 ptr_。         ~unique_ptr() 析构函数会自动调用 delete 释放动态分配的对象。

        release() 函数可以释放 unique_ptr 对象所持有的指针,并将该指针返回。

        operator* 和 operator-> 函数可以像普通指针一样使用 unique_ptr 对象,get() 函数可以获取 unique_ptr 对象所持有的指针。

2.2.2 unique_ptr使用方式

        1. 创建unique_ptr并为其分配内存

std::unique_ptr<int> ptr1(new int(10));

        2.  访问 unique_ptr 对象所指向的内存:

int x = *ptr1;
std::cout << x << std::endl;  // Output: 10

        3.  释放 unique_ptr 对象所持有的指针:

int* raw_ptr = ptr1.release();

        4.  移动语义

std::unique_ptr<int> ptr2(new int(20));
std::unique_ptr<int> ptr3 = std::move(ptr2);

2.3 weak_ptr

2.3.1 原理及简单实现

template<typename T>
class weak_ptr {
 public:
  weak_ptr() : ptr_(nullptr), count_(nullptr) {}
  weak_ptr(const shared_ptr<T>& other) : ptr_(other.ptr_), count_(other.count_) {}
  
  T* get() const {
    if (count_ && *count_ > 0) {
      return ptr_;
    }
    return nullptr;
  }

  shared_ptr<T> lock() const {
    if (count_ && *count_ > 0) {
      return shared_ptr<T>(ptr_, count_);
    }
    return shared_ptr<T>();
  }

 private:
  T* ptr_;
  int* count_;
};

        weak_ptr 类包含了一个指向 T 类型对象的指针 ptr_ 和一个指向引用计数的指针 count_。

        get() 函数返回所指向的对象的指针,但是会检查引用计数是否大于0.

        lock()函数可以返回一个 shared_ptr 对象, 如果引用计数大于0,返回一个新的 shared_ptr 对象,否则返回一个空的 shared_ptr 对象。

weak_ptr(const shared_ptr<T>& other) : 
    ptr_(other.ptr_), count_(other.count_) {}

        这段代码是 weak_ptr 类的构造函数,它接受一个 shared_ptr<T> 类型的参数 other。通过 other 对象获取其管理的对象的指针 ptr_ 和引用计数指针 count_。然后将它们分别赋值给 weak_ptr 类的成员变量 ptr_ 和 count_。

2.3.2 weak_ptr的作用何在?

        为解决循环引用问题。

        循环引用是指两个或多个对象之间相互引用,而这些对象都不能被释放,因为它们都被另一个对象所引用。这样的话,这些对象就会成为内存泄露,导致程序无法正常工作。

        而 weak_ptr 就可以在这种情况下解决这个问题。它可以访问一个 shared_ptr 所管理的对象,但不会增加该对象的引用计数。这样就可以解除循环引用,使得这些对象能够正常被释放。

        举个例子,假设有一个类A,它有一个指针成员变量指向另一个类B, 类B也有一个指针成员变量指向类A,这样就出现了循环引用。 而使用 weak_ptr 就可以解除这种循环引用,使得两个类都能够正常被释放。

class A;
class B {
  std::weak_ptr<A> a_ptr;
};

class A {
  std::shared_ptr<B> b_ptr;
};

        在这个例子中,类A和类B互相持有对方的指针,但是使用了 weak_ptr 就可以解除循环引用。当类A的对象被销毁时,类B的 weak_ptr 对象不会增加A的引用计数,这样就不会出现循环引用。反之亦然。

2.3.3 具体使用方法

#include <iostream>
#include <memory>

class A;

class B {
public:
    std::weak_ptr<A> a_ptr;
};

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() {std::cout << "A is deleted" << std::endl;}
};

int main() {
    std::shared_ptr<A> a_ptr(new A);
    std::shared_ptr<B> b_ptr(new B);
    a_ptr->b_ptr = b_ptr;
    b_ptr->a_ptr = a_ptr;
    return 0;
}

        类A和类B互相持有对方的指针,但是使用了 weak_ptr 就可以解除循环引用。当类A的对象被销毁时,类B的 weak_ptr 对象不会增加A的引用计数,这样就不会出现循环引用,从而可以避免内存泄漏。

        运行结果为: A is deleted

        在 weak_ptr 对象转换为 shared_ptr 对象之前,应该先使用 expired() 或 lock() 函数检查原先的 shared_ptr 对象是否已经销毁。

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> shared_ptr_obj(new int);
    std::weak_ptr<int> weak_ptr_obj(shared_ptr_obj);
    if (weak_ptr_obj.expired()) {
        std::cout << "shared_ptr object has been deleted" << std::endl;
    } else {
        std::cout << "shared_ptr object is still alive" << std::endl;
    }
    shared_ptr_obj.reset();
    if (weak_ptr_obj.expired()) {
        std::cout << "shared_ptr object has been deleted" << std::endl;
    } else {
        std::cout << "shared_ptr object is still alive" << std::endl;
    }
    return 0;
}

        结果:

shared_ptr object is still alive
shared_ptr object has been deleted

        

        lock() 函数返回一个 shared_ptr 对象,如果引用的对象已经被销毁,则返回一个空的 shared_ptr 对象。

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> shared_ptr_obj(new int);
    std::weak_ptr<int> weak_ptr_obj(shared_ptr_obj);
    std::shared_ptr<int> locked_ptr = weak_ptr_obj.lock();
    if (locked_ptr) {
        std::cout << "shared_ptr object is still alive" << std::endl;
    } else {
        std::cout << "shared_ptr object has been deleted" << std::endl;
    }
    shared_ptr_obj.reset();
    locked_ptr = weak_ptr_obj.lock();
    if (locked_ptr) {
        std::cout << "shared_ptr object is still alive" << std::endl;
    } else {
        std::cout << "shared_ptr object has been deleted" << std::endl;
    }
    return 0;
}

shared_ptr object is still alive
shared_ptr object has been deleted

        这两个函数都可以用来检查原先的 shared_ptr 对象是否已经销毁,但是在使用时应根据具体情况来选择。如果只是需要知道原先的 shared_ptr 对象是否已经销毁,那么可以使用 expired() 函数。如果需要在原先的 shared_ptr 对象未销毁时访问其所管理的对象,那么可以使用 lock() 函数。

三、总结

        智能指针的出现类似于加入了垃圾回收机制,一般来讲shared_ptr是日常使用最多的。使用智能指针可以提高程序的安全性和可读性,减少内存泄露和空指针错误。但是,智能指针会带来一些额外的开销,对性能有一定的影响。

        首先,智能指针会增加对象的大小,因为需要存储引用计数和其他元数据。其次,使用智能指针会带来额外的内存分配和释放操作,当创建和销毁智能指针对象时会引起额外的内存开销。最后,使用智能指针会增加锁的使用,导致线程同步时产生额外的开销。总的来说,使用智能指针会带来一定的性能开销,但是能够带来的好处远大于开销。如果性能是关键的话,可以考虑使用自己实现的智能指针,或者在高性能代码中使用原始指针。

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

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

相关文章

[数据结构基础]链式二叉树及其前序、中序和后序遍历

一. 链式二叉树的结构和实现 1.1 链式二叉树的结构 链式二叉树&#xff0c;即使用链来表示一颗二叉树。链式二叉树的存储又可分为二叉链和三叉链&#xff0c;其中二叉链存储节点数据、指向左子节点的指针和指向右子节点的指针&#xff0c;三叉链相对于二叉链多存储指向父亲节…

一种基于肌电信号运动起点、波峰、终点实时自动检测的方法

一种基于肌电信号运动起点、波峰、终点实时自动检测的方法 (⊙o⊙)…,这篇是我写收费文章的第一篇。咱也尝试下知识付费,哈哈。 先看下效果,在给定理想正弦波的情况下,可以准确识别到正弦波的起点、波峰和终点。机器实拍图如下。 因为我的实际环境没有专利里面那么复杂,所…

Android 蓝牙开发——基础开发(三)

蓝牙开发这部分主要以 APP 端调用功能接口为开始&#xff0c;到 Framework 端的调用流程&#xff0c;最后到调用状态机结束&#xff0c;不涉及蓝牙协议栈的开发分析。 一、BluetoothAdapter 1、APP获取适配器 蓝牙权限 <mainifest><uses-permission android:name&…

3.ESP32-S2/S3 USB 挂载SPI-SD,当作U盘使用,无线U盘

使用的 IDF_4.4 C语言开发 1.ESP32-S2/S3 USB烧录 输出日志 2.ESP32-S2/S3 USB 挂载内部Flash&#xff0c;当作U盘使用&#xff0c;无线U盘 3.ESP32-S2/S3 USB 挂载SPI-SD&#xff0c;当作U盘使用&#xff0c;无线U盘 目录1.打开 usb_msc_wireless_disk 工程 Confinguration2.…

数学建模-数学规划(Matlab)

目录 一、线性规划求解 二、非线性规划问题 三、整数规划&#xff08;包括0-1规划&#xff09; 四、最大最小化模型 五、多目标规划模型 注意&#xff1a;代码文件仅供参考&#xff0c;一定不要直接用于自己的数模论文中国赛对于论文的查重要求非常严格&#xff0c;代码雷同…

微服务Spring Boot 整合 Redis 实现 UV 数据统计

文章目录⛄引言一、HyperLoglog基础用法⛅HyperLoglog 基本语法、命令⚡HyperLoglog 命令完成功能实现二、UV统计 测试百万数据的统计☁️什么是UV统计⚡使用SpringBoot单元测试进行测试百万数据统计⛵小结⛄引言 本文参考黑马 点评项目 在各个项目中&#xff0c;我们都可能需…

DaVinci:限定器 - HSL

调色页面&#xff1a;限定器Color&#xff1a;Qualifier限定器 - HSL Qualifier - HSL根据色相、饱和度和亮度等来选择画面上的对应区域&#xff0c;从而将二级调色与修饰限制在一定的范围。选择范围Selection Range拾取器Picker在检视器画面上按住并拖动&#xff0c;可以选择相…

Linux FHS结构

FHS是Filesystem Hierarchy Standard&#xff08;文件系统层次化标准&#xff09;的缩写&#xff0c;多数Linux版本采用这种文件组织形式&#xff0c;类似于Windows操作系统中c盘的文件目录&#xff0c;FHS采用树形结构组织文件。FHS定义了系统中每个区域的用途、所需要的最小构…

格式化输出

1、golang不同输出语句的区别&#xff1a; 特点PrintPrintlnPrintf输出内容到控制台&#xff08;终端输出&#xff09;SprintSprintlnSprintf输出内容为字符串FprintFprintlnFprintf输出内容到文件特点 输出内容不会换行。 不能格式化字符串。 输出内容换行,。 不能格式化字符…

【C语言进阶】枚举与联合体

目录一&#xff1a;枚举1.1&#xff1a;枚举类型的定义&#xff1a;1.1&#xff1a;枚举的优点&#xff1a;1.2&#xff1a;枚举的使用&#xff1a;二&#xff1a;联合&#xff08;共用体&#xff09;2.1&#xff1a;联合类型的定义&#xff1a;2.2&#xff1a;联合类型的特点&…

cin、cin.getline(arr, size)、getline(cin, str)

一、cin使用空白&#xff08;空格、制表符、换行符&#xff09;来确定字符串的截止位置 注意下方这段代码 使用cin来接收姓名和甜点名&#xff0c;当我的名字长度大于一个单词长度时&#xff0c;cin直接按空格进行接收&#xff0c;将我输入的xiao接收到name中&#xff0c;wei接…

swiftUI coreml deeplabv3去除背景

现在手机的性能越来越好&#xff0c;好多深度学习的框架都能能够跑在手机上。因此就集成一下一个官方的深度学习model试一下。 其他的框架生成的模型都能通过相应的工具转换成mlmodel用&#xff0c;转换也比较简单。 下面以替换图像去背景为例&#xff0c;不过官方模型这个效果…

概论_第3章_二维随机变量__边缘概率密度

边缘概率密度是二维随机变量中的重点内容&#xff0c; 经常作为一个重要的考点&#xff0c; 必须掌握。一 定义对二维随机变量(X, Y) ,分量X, 或者Y的概率密度称为 (X, Y)的边缘概率密度&#xff0c;简称边缘密度&#xff0c;记为 或者 。边缘密度 或者 可由 二维随机变量的密…

什么是pod(容器组)

pod&#xff08;容器组&#xff09; 术语中英文对照&#xff1a; 英文全称英文缩写中文翻译PodPod容器组ContainerContainer容器ControllerController控制器 什么是 Pod 容器组&#xff1f; Pod&#xff08;容器组&#xff09;是 Kubernetes 中最小的可部署单元。一个 Pod&a…

基于FPGA的UDP 通信(六)

引言 前文链接&#xff1a; 基于FPGA的UDP 通信&#xff08;一&#xff09; 基于FPGA的UDP 通信&#xff08;二&#xff09; 基于FPGA的UDP 通信&#xff08;三&#xff09; 基于FPGA的UDP 通信&#xff08;四&#xff09; 基于FPGA的UDP 通信&#xff08;五&#xff09;…

【Spring6源码・AOP】AOP源码解析

上一篇《【Spring6源码・AOP】代理对象的创建》&#xff0c;我们知道了代理是如何创建的&#xff0c;那么它又是如何工作的呢&#xff1f; 创建完代理对象之后&#xff0c;最终&#xff0c;会真正的执行我们的目标方法&#xff0c;但是步入该方法&#xff0c;会进入cglib代理类…

ET框架关于opCode的理解

因为所有的网络消息在发送时候格式都是这样 对于用Protobuf定义的每一消息类型class&#xff0c;都需要定义一个对应消息头code在发送的时候&#xff0c;先将消息体进行序列化&#xff0c;再将code进行序列化&#xff0c;进行组装发送 //这个代码没有进行过优化&#xff0c;会产…

大衣哥给儿媳买回来烟花,是准备加入河南炮击山东大战吗

自从取得抗疫阶段性胜利后&#xff0c;国人就再也难以按捺激动的心情&#xff0c;都想通过放烟花以示庆祝。河南山东属于搭界的两个省&#xff0c;最近就因为放烟花&#xff0c;闹出来不小的笑话&#xff0c;有人甚至戏称炮击事件。 事情的起因是这样的&#xff0c;河南因为地处…

maven的build节点配置

虽然一直在使用maven&#xff0c;但是对于maven的配置还没有深入的了解过。本文以build节点为切入点&#xff0c;主要解释相关maven打包使用到的一些基础配置。 文章目录build节点常用插件spring-boot-maven-pluginmaven-jar-pluginmaven-dependency-plugin注意事项build节点 …

基于javaweb的会议管理系统源码+数据库,javaEE会议管理系统源码

guihaiyidao_git 介绍 javaEE工程 普通的javaEE工程&#xff0c;用idea打开工程即可运行 服务器用的是Tomcat 8.5.722 数据库用的Oracle xe版 数据库可视化工具使用的是PLSQL 相关软件 需要可从百度网盘中获取 链接&#xff1a;https://pan.baidu.com/s/1ZrmfsvQEA4dIP0GF_p…