[C++]:特殊类的设计

1. 不可拷贝类

我们知道,某些资源只能有一个对象持有,拷贝可能导致资源混乱。例如智能指针std::unique_ptr独占管理动态分配对象,文件句柄、网络套接字、数据库连接等资源通常也是独占的,不允许拷贝。

在C++11之前,要创建一个不可拷贝的类,通常的做法是将拷贝构造函数和赋值运算符重载声明为private,并且只进行声明而不提供定义。例如下面这个NonCopyable类:

class NonCopyable
{
public:NonCopyable() {}
private:// C++11之前,只声明不实现NonCopyable (const NonCopyable&){}NonCopyable& operator=(const NonCopyable&){}
};

当代码中某个地方尝试对该类的对象进行拷贝构造时,由于拷贝构造函数和赋值运算符重载是私有的,在类外部无法访问这些私有成员函数,所以从访问权限层面就阻止了拷贝行为。而只声明不实现的原因在于,如果只是声明为私有但不小心提供了定义,那么在类内部或者友元函数中还是有可能进行拷贝操作的,从而彻底禁止拷贝行为。

C++11引入了delete关键字,利用这个关键字可以更清晰、简洁地实现不可拷贝的类。像下面这样的NonCopyable类定义:

class NonCopyable {
public:NonCopyable() = default;// 使用delete关键字禁用拷贝构造函数和赋值运算符NonCopyable(const NonCopyable&) = delete;NonCopyable& operator=(const NonCopyable&) = delete;void doSomething() {//...}
};

通过将拷贝构造函数和赋值运算符重载直接标记为= delete,明确告知编译器要禁用这两个函数。当代码中出现对该类对象进行拷贝构造或者赋值的操作时,编译器会直接报错,指出对应的函数已被删除,不允许调用,从而实现禁止拷贝的功能。

我们还可以定义一个通用的不可拷贝的基类(如nocopy类),然后让具体需要禁止拷贝的类去继承这个基类,示例代码如下:

class nocopy
{
public:nocopy() {}nocopy(const nocopy&) = delete;const nocopy& operator=(const nocopy&) = delete;
};// 继承
class NonCopyable:public nocopy
{
public:NonCopyable() {};
private:
};

当派生类(如NonCopyable类)的对象进行拷贝构造或者赋值操作时,在派生类的拷贝构造函数和赋值运算符重载的实现过程中(即使没有显式定义,编译器也会默认生成相应的函数,尝试调用基类的对应函数来处理基类部分的拷贝和赋值逻辑),会先在初始化列表中去调用基类的拷贝构造函数和赋值运算符重载。但由于基类(nocopy类)已经通过delete关键字(或者类似在C++11之前将其设为私有等方式)禁止了这些操作,所以派生类也就无法顺利完成拷贝构造和赋值操作,从而实现了不可拷贝的效果。

2. 只能在堆上创建的类

对于一些对象大小在编译时未知或生命周期不确定时,动态内存分配更合适。并且多个模块或对象需共享同一实例,堆上对象可通过指针或引用共享。例如大型数据结构(大数组、链表、树等)、资源管理器(文件处理类、数据库连接类等)、单例模式(确保对象唯一性和生命周期管理)。所以我们有时需要设计一个只能在堆上创建的类,一般我们有两种方式实现:

以下是关于只能在堆上创建的类的详细介绍,包含其不同实现方式、原理、优缺点以及使用时的注意事项等内容:

首先第一种方式,我们可以通过私有构造函数结合静态创建方法实现

class HeapOnly {
public:// 静态成员函数用于在堆上创建对象并返回指针static HeapOnly* create() {return new HeapOnly();}
private:// 构造函数私有化HeapOnly() {}// 拷贝构造函数私有化,禁止拷贝构造HeapOnly(const HeapOnly&) = delete;// 拷贝赋值运算符私有化,禁止赋值操作HeapOnly& operator=(const HeapOnly&) = delete;
};

由于构造函数是私有的,像HeapOnly obj;这样直接在栈上创建对象的语句就无法通过编译,因为编译器会检查到在类外部没有权限调用构造函数。而对于拷贝构造函数和拷贝赋值运算符也进行了私有化或者删除的处理,避免了通过拷贝操作来间接创建对象的可能性,保证了对象创建的唯一性和可控性,只能按照规定的静态create函数在堆上创建。

第二种方式我们可以通过私有化析构函数实现

  • 实现方式
class HeapOnly {
public:HeapOnly() = default;void Destroy(){delete this;}
private:// 析构函数私有化~HeapOnly() {}// 拷贝构造函数私有化,禁止拷贝构造HeapOnly(const HeapOnly&) = delete;// 拷贝赋值运算符私有化,禁止赋值操作HeapOnly& operator=(const HeapOnly&) = delete;
};

对于栈上对象,编译器会自动在对象生命周期结束(比如离开作用域)时调用析构函数进行资源清理等操作,但因为析构函数是私有的,在类外部代码中没有访问权限去执行这个操作,所以直接在栈上创建对象的语句就无法编译通过。而在堆上创建对象时,内存分配由new操作符完成,只是后续需要手动释放内存,通过提供Destroy函数来解决手动释放的问题,从而强制使对象只能在堆上创建并按照规定的方式进行内存释放管理。

3. 只能在栈上创建的类

栈上创建对象的生命周期与包含函数的作用域相同,函数结束时自动销毁,无需显式调用析构函数。并且栈上内存分配比堆上快,无需额外内存管理开销。例如锁类、作用域守卫类常设计为只能在栈上创建。所以有时候我们也需要设计一个只能在栈上创建的类。

我们首先肯定想到的是将构造函数或析构函数声明为私有,提供静态方法创建对象并返回。

//错误示例
class StackOnly {
public:static StackOnly Create(){static StackOnly obj;return obj;}
private:// 构造函数私有化StackOnly() {}
};
int main()
{//StackOnly* obj = new StackOnly(); errorStackOnly obj = StackOnly::Create(); //间接在堆上创建对象StackOnly* n = new StackOnly(obj); //okreturn 0;
}

但此方法不能完全防止使用new创建对象,因为静态工厂方法返回对象时可能会用到拷贝构造函数(编译器可能优化),所以不能禁用/私有拷贝构造函数。而一旦不能禁止拷贝构造我们可以使用类似 StackOnly* obj = new(栈上创建的对象),调用拷贝构造间接创建一个堆上的对象,所以这种方式其实是不可取的。

所以我们可以换种思路, 既然要禁止在堆上开辟空间来创建类对象,我们可以采取直接禁止使用newdelete操作符的思路。因为在C++中,newdelete默认会调用全局的operator newoperator delete函数来进行内存分配和释放操作。所以,我们只需在类内对operator new进行重载,并通过显式删除的方式将其禁用,同时也把operator delete删除掉,如此一来,就能确保无法通过newdelete这两个操作符去创建该类的对象了。

class StackOnly {
public:StackOnly() = default;~StackOnly() = default;private:// 显式删除operator new操作符,禁止在堆上通过new创建对象void* operator new(std::size_t) = delete;// 显式删除operator delete操作符,禁止在堆上释放对象void operator delete(void*) = delete;// 同样也可以删除数组形式的new和delete操作符,防止通过new[]和delete[]来操作对象void* operator new[](std::size_t) = delete;void operator delete[](void*) = delete;
};int main() {// 正确的创建方式,在栈上创建对象StackOnly obj;// 以下方式会报错,因为new和delete操作符被删除,不能在堆上创建和释放对象// StackOnly* ptr = new StackOnly();// delete ptr;// StackOnly* arr = new StackOnly[5];// delete[] arr;return 0;
}

4. 不可被继承的类

在C++98标准下,可以通过将类的构造函数声明为private来阻止类被继承。因为当一个类想要继承另一个类(作为子类)时,子类的构造函数会默认(隐式或显式地)调用父类的构造函数来初始化从父类继承下来的那部分成员。如果父类的构造函数是private的,子类就无法访问它,也就没办法完成这个初始化过程,从而导致编译错误,实现了禁止继承的效果。

以如下代码为例:

class NonInherit
{
public:static NonInherit GetInstance(){return NonInherit();}
private:NonInherit(){}
};

这里定义了NonInherit类,它的构造函数是私有的,唯一对外提供了一个静态成员函数GetInstance用于获取该类的实例(返回一个临时的NonInherit对象)。

C++11引入了final关键字,当在类定义时使用final关键字修饰该类,就明确告知编译器这个类是不允许被继承的,例如:

class A final
{// 类的成员定义等内容
};

如果后续有其他类试图去继承这个被final修饰的类,像下面这样:

class B : public A
{// 编译会报错,因为A类不能被继承
};

编译器会直接报错,提示类Afinal的,不允许作为基类被继承,这种方式更加直观、明确地表达了类不可继承的意图,而且语法简洁明了。

5. 控制可创建对象的类

有时候我们也需要一个可以控制可创建对象的类:

首先我们将禁止构造函数,拷贝构造函数以及赋值重载。并单独提供一个静态方法来创建对象,方便我们管理。

class LimitedObjectCreator {
public:// 静态方法用于创建对象,返回指向创建对象的指针static LimitedObjectCreator* create() {if (count > 0) {--count;std::cout << "对象创建成功,还可创建的对象数量:" << count << std::endl;return new LimitedObjectCreator();}else {std::cerr << "已超出对象创建限制数量,无法创建对象。" << std::endl;return nullptr;}}// 析构函数,用于释放对象资源,这里可以添加具体的资源释放逻辑(如果有需要)~LimitedObjectCreator() {++count;std::cout << "对象已销毁,还可创建的对象数量:" << count << std::endl;}static void setObjectLimit(int num) {count = num;}private:// 将构造函数声明为私有,禁止外部直接调用构造函数创建对象LimitedObjectCreator() {}// 拷贝构造函数也声明为私有,禁止拷贝构造LimitedObjectCreator(const LimitedObjectCreator&) = delete;// 赋值运算符重载同样声明为私有,禁止赋值操作LimitedObjectCreator& operator=(const LimitedObjectCreator&) = delete;static int count;
};// 初始化静态成员变量
int LimitedObjectCreator::count = 0;int main() {LimitedObjectCreator::setObjectLimit(3);LimitedObjectCreator* obj1 = LimitedObjectCreator::create();LimitedObjectCreator* obj2 = LimitedObjectCreator::create();LimitedObjectCreator* obj3 = LimitedObjectCreator::create();LimitedObjectCreator* obj4 = LimitedObjectCreator::create();if (obj1 != nullptr) {delete obj1;}if (obj2 != nullptr) {delete obj2;}if (obj3 != nullptr) {delete obj3;}if (obj4 != nullptr) {delete obj4;}return 0;
}

我们可以通过一个静态成员变量 count来控制可创建对象的个数,其可以通过专门的函数 setObjectLimit设置,每次创建时可创建对象个数会减一--count,释放资源时可创建对象个数会相应增一++count

6. 单例模式

单例模式是一种设计模式,其核心特点是一个类只能创建一个对象,该模式可以保证系统中该类仅有一个实例,并提供一个访问它的全局访问点,此实例能被所有程序模块共享。

例如在服务器程序中,服务器的配置信息存放在文件里,可由一个单例对象统一读取配置数据,服务进程中的其他对象再通过这个单例对象获取配置信息,这简化了复杂环境下的配置管理。

单例模式有饿汉模式和懒汉模式两种实现方式:

6.1 饿汉模式

饿汉模式的核心思想是在程序启动阶段(也就是 main 函数执行之前)就创建单例类的唯一实例对象,无论后续程序运行过程中是否会实际使用到这个实例。这种方式就好像一个人很饿,提前把食物都准备好,不管后面吃不吃。

  • 优点:实现起来较为简单直观,代码结构清晰,不需要考虑复杂的线程同步问题,因为实例在一开始就创建好了,后续只是获取这个已存在的实例而已。
  • 缺点:可能会导致进程启动变慢,尤其是当单例对象的构造过程比较复杂、耗时(例如需要加载大量配置文件、初始化很多资源等),在程序启动时就执行这些操作会拖慢启动速度。另外,当存在多个单例类对象实例时,它们的创建顺序是不确定的,这在一些对实例初始化顺序有严格要求的场景下可能会带来问题。
namespace Hungry_Man {// 饿汉模式--main函数之前就创建对象class Singleton {public:static Singleton* GetInstance() {return &_inst;}private:Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;Singleton() {}static Singleton _inst;};Singleton Singleton::_inst;
}

在这个代码中,Singleton 类定义了一个私有的静态成员变量 _inst,它就是单例类的唯一实例。在类外进行了 Singleton::_inst 的定义,这使得在程序启动阶段,这个实例就会被创建出来(在 main 函数执行之前就已经存在了)。

GetInstance 函数很简单,它只是返回这个已经创建好的实例的地址。由于构造函数被声明为私有,外部无法随意创建 Singleton 类的其他对象,保证了整个程序中只有这一个实例存在,符合单例模式的要求。而且通过 delete 关键字删除了拷贝构造函数和赋值运算符重载函数,防止了通过拷贝或赋值的方式产生额外的对象实例。

6.2 懒汉模式

懒汉模式采取的是延迟加载的策略,只有在第一次真正需要使用单例对象时才去创建它,就好比一个人很懒,等到要吃东西了才去准备食物。这种模式适用于单例对象构造比较耗时或者占用资源较多(比如加载插件、初始化网络连接、读取文件等情况),并且程序运行时有可能根本不会用到该对象的场景,这样可以避免在程序启动阶段就消耗不必要的资源。

  • 优点:因为是在第一次使用时才创建对象,所以不会影响程序启动的速度,进程启动时没有额外的负载。同时,多个单例实例的启动顺序可以根据实际使用的先后情况自由控制,比较灵活。
  • 缺点:实现相对复杂一些,需要充分考虑线程安全问题,在多线程环境下,如果多个线程同时尝试获取单例对象,要保证只有一个线程能够创建实例,避免重复创建。另外,还需要考虑对象的释放问题,要合理地进行内存管理,确保单例对象在合适的时候被正确释放,避免内存泄漏等问题。
namespace Lazy_MAN {// 懒汉模式class Singleton {public:static Singleton* GetInstance() {// 双检查加锁方式,if (_pinst == nullptr) {             // 第一次判断是防止对象创建好以后,还要每次加锁,就浪费了unique_lock<mutex> lock(_mtx);if (_pinst == nullptr) {       // 第二次判断是为了防止多个线程一起写不安全现象_pinst = new Singleton;}}return _pinst;}static void DelInstance() {delete _pinst;_pinst = nullptr;}class GC{public:~GC(){Singleton::DelInstance();}};private:Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;static Singleton* _pinst;static mutex _mtx;static GC gc;// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象};Singleton* Singleton::_pinst = nullptr;mutex Singleton::_mtx;
}

值得强调的是在 GetInstance 函数中采用了双检查加锁机制:

  • 第一次 if (_pinst == nullptr) 判断:从性能优化角度出发,如果单例对象已经被创建(即 _pinst 不为 nullptr),那么直接返回已存在的实例即可,无需再进行加锁和后续创建实例的操作。因为加锁解锁本身是有一定开销的,如果每次调用 GetInstance 都进行加锁,会影响程序性能,尤其是在单例对象已经创建好的情况下,这种开销是不必要的。
  • 第二次 if (_pinst == nullptr) 判断:主要是从线程安全角度考虑,在多线程环境下,可能会出现多个线程同时通过了第一次 if 判断(因为此时 _pinst 确实为 nullptr,单例对象还未创建),然后这些线程都尝试获取锁并进入到临界区(由 unique_lock 保护的代码块)。如果没有第二次 if 判断,那么每个线程都会执行 _pinst = new Singleton; 这一语句,从而导致创建多个单例对象,违背了单例模式的初衷。而第二次判断确保只有一个线程能够真正执行创建单例对象的操作,其他线程在等待锁释放后,再次检查 _pinst 时,会发现单例对象已经被创建,从而避免重复创建,保证了在多线程环境下单例对象的唯一性。

除此之外,我们还定义了一个内部类 GC,其析构函数中调用了 DelInstance 函数,这样在程序结束时,系统会自动调用 GC 的析构函数,进而释放单例对象,解决了对象释放的问题,避免内存泄漏。同时,通过将拷贝构造函数和赋值运算符重载函数声明为 delete,同样防止了外部创建多个对象实例的情况,保证单例模式的正确实现。

当然我们也可以采用如下方式实现一个简单的懒汉单例模式:

class Singleton
{
public:static Singleton& GetInstance(){//C++11之后局部静态变量是线程安全的static Singleton inst;return inst;}
private:Singleton() = default;Singleton(const Singleton&) = delete;Singleton&operator=(const Singleton&) = delete;
};
int main()
{Singleton &inst = Singleton::GetInstance();return 0;
}

最后懒汉模式和饿汉模式的区别总结如下:

  • 懒汉模式需要考虑线程安全和释放问题,实现相对复杂;饿汉模式不存在这些问题,实现简单。
  • 懒汉是懒加载模式,在需要时初始化创建对象,不影响程序启动;饿汉模式在程序启动阶段就创建初始化实例对象,可能导致程序启动慢,影响体验。
  • 如果有多个单例类且存在依赖关系(如B依赖A,要求A单例先创建初始化,B单例再创建初始化),则不能用饿汉模式(无法保证创建初始化顺序),这时懒汉模式可手动控制。

而且在实际应用中,懒汉模式通常更实用。

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

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

相关文章

青训营刷题笔记16

问题描述 小R从班级中抽取了一些同学&#xff0c;每位同学都会给出一个数字。已知在这些数字中&#xff0c;某个数字的出现次数超过了数字总数的一半。现在需要你帮助小R找到这个数字。 测试样例 样例1&#xff1a; 输入&#xff1a;array [1, 3, 8, 2, 3, 1, 3, 3, 3] 输出…

Go语言链接Redis数据库

1.使用go get命令安装go-redis/v8库&#xff1a; 我这里使用的vscode工具安装&#xff1a; go get github.com/go-redis/redis/v82.创建Redis客户端实例 使用以下Go代码连接到Redis服务器并执行命令&#xff1a; package mainimport ("context""fmt"&q…

Mybatis 核心配置文件

MyBatis的全局配置文件mybatis-config.xml&#xff0c;配置内容如下&#xff1a; properties&#xff08;属性&#xff09; settings&#xff08;全局配置参数&#xff09; typeAliases&#xff08;类型别名&#xff09; typeHandlers&#xff08;类型处理器&#xff09; obj…

09 —— Webpack搭建开发环境

搭建开发环境 —— 使用webpack-dev-server 启动Web服务&#xff0c;自动检测代码变化&#xff0c;有变化后会自动重新打包&#xff0c;热更新到网页&#xff08;代码变化后&#xff0c;直接替换变化的代码&#xff0c;自动更新网页&#xff0c;不用手动刷新网页&#xff09; …

TCP vs UDP:如何选择适合的网络传输协议?

在网络通信中&#xff0c;TCP&#xff08;Transmission Control Protocol&#xff09;和UDP&#xff08;User Datagram Protocol&#xff09;是两种非常重要的传输层协议。它们各有特点&#xff0c;适用于不同类型的应用场景。本文将详细探讨TCP和UDP协议的结构、优缺点及应用&…

网络安全之内网安全

下面给出了应对企业内网安全挑战的10种策略。这10种策略即是内网的防御策略&#xff0c;同时也是一个提高大型企业网络安全的策略。 1、注意内网安全与网络边界安全的不同 内网安全的威胁不同于网络边界的威胁。网络边界安全技术防范来自Internet上的攻击&#xff0c;主要是防…

7-2 扑克牌花色

作者 李祥 单位 湖北经济学院 给 52 张扑克牌面编号如下&#xff1a; 编号牌面编号牌面编号牌面编号牌面0♠A13♥A26♣A39♦A1♠214♥227♣240♦22♠315♥328♣341♦33♠416♥429♣442♦44♠517♥530♣543♦55♠618♥631♣644♦66♠719♥732♣745♦77♠820♥833♣846♦88♠9…

windows 中docker desktop 安装

前提条件&#xff1a; 安装wsl2 1. 下载 Docker Desktop 访问 Docker Desktop 官方下载页面。 https://www.docker.com/products/docker-desktop/ 根据你的操作系统架构&#xff08;一般为 Windows x86_64&#xff09;下载安装程序。 选择标准&#xff1a; AMD64 是行业…

初学 flutter 环境变量配置

一、jdk&#xff08;jdk11&#xff09; 1&#xff09;配置环境变量 新增&#xff1a;JAVA_HOMEC:\Program Files\Java\jdk-11 //你的jdk目录 在path新增&#xff1a;%JAVA_HOME%\bin2&#xff09;验证是否配置成功&#xff08;cmd运行命令&#xff09; java java -version …

Linux——进程间通信之管道

进程间通信之管道 文章目录 进程间通信之管道1. 进程间通信1.1 为什么要进行进程间的通信1.2 如何进行进程间的通信1.3 进程间通信的方式 2. 管道2.1 匿名管道2.1.1 系统调用pipe()2.1.2 使用匿名管道进行通信2.1.1 匿名管道四种情况2.1.2 匿名管道的五大特性2.1.3 进程池 2.2 …

Sigrity SPEED2000 DDR simulation模式如何生成和解读DDR仿真报告-SODIMM-Write模式

Sigrity SPEED2000 DDR simulation模式如何生成和解读DDR仿真报告-SODIMM-Write模式 Sigrity SPEED2000 DDR simulation模式如何进行DDR仿真分析操作指导-SODIMM-Write模式详细介绍了如何进行DDR Write模式的仿真分析,下面基于此仿真结果进行DDR报告的输出和解读分析 在workfl…

【机器学习chp7】SVM

参考1&#xff0c;笔记 SVM笔记.pdf 参考2&#xff1a;王木头视频 什么是SVM&#xff0c;如何理解软间隔&#xff1f;什么是合叶损失函数、铰链损失函数&#xff1f;SVM与感知机横向对比&#xff0c;挖掘机器学习本质_哔哩哔哩_bilibili 目录 一、SVM模型 二、构建决策函…

使用Electron将vue2项目打包为桌面exe安装包

目录 一、下载electron模板项目 【electron-quick-start】​ 二、打开项目&#xff0c;安装所有依赖 三、在打exe包的时候报错是因为没有&#xff0c;需要检查并安装之后重新打包&#xff1b; 四、经过这么疯狂的一波操作之后&#xff0c;就可以打包出你想要的exe安装包&am…

摄像机常见的问题及解决方法

文章目录 1)红外网络枪形摄像机白天出现模糊&#xff0c;晚上出现星芒灯2、摄像机夜晚效果调整3、网络摄像机帧率和码流调整4、码流对图像质量的影响 如果你在安装的过程中,出现了以下的问题,请对照下列描述解决你的问题&#xff1a; 1)红外网络枪形摄像机白天出现模糊&#xf…

决策树分类算法【sklearn/决策树分裂指标/鸢尾花分类实战】

决策树分类算法 1. 什么是决策树&#xff1f;2. DecisionTreeClassifier的使用&#xff08;sklearn&#xff09;2.1 算例介绍2.2 构建决策树并实现可视化 3. 决策树分裂指标3.1 信息熵&#xff08;ID3&#xff09;3.2 信息增益3.3 基尼指数&#xff08;CART&#xff09; 4. 代码…

001 数字逻辑概论

1.1 数字信号与数字电路 目标1&#xff1a;what is 数字信号与数字电路 1.1.1.数字技术的发展及其应用 &#xff08;1&#xff09;发展&#xff1a; 发展过程特点: 以电子器件的发展为基础&#xff0c;如下图 电子管时代&#xff1a; 电子管&#xff1b;电子管体积大、重量…

Rust中Tracing 应用指南

欢迎来到这篇全面的Rust跟踪入门指南。Rust 的tracing是一个用于应用程序级别的诊断和调试的库。它提供了一种结构化的、异步感知的方式来记录日志和跟踪事件。与传统的日志记录相比&#xff0c;tracing能够更好地处理复杂的异步系统和分布式系统中的事件跟踪&#xff0c;帮助开…

C语言——break、continue、goto

目录 一、break 二、continue 1、在while循环中 2、在for循环中 三、go to 一、break 作用是终止循环&#xff0c;在循环内遇到break直接就跳出循环。 注&#xff1a; 一个break语句只能跳出一层循环。 代码演示&#xff1a; #include<stdio.h>void test01() {for (…

SSM全家桶 1.Maven

或许总要彻彻底底地绝望一次 才能重新再活一次 —— 24.11.20 maven在如今的idea中已经实现自动配置&#xff0c;不需要我们手动下载 一、Maven的简介和快速入门 Maven 是一款为 Java 项目构建管理、依赖管理的工具(软件)&#xff0c;使用 Maven 可以自动化构建测试、打包和发…

Oracle SQL*Plus中的SET VERIFY

在 Oracle SQL*Plus 中&#xff0c;SET VERIFY ON 和 SET VERIFY OFF 是两个用于控制命令执行前后显示变量值的命令。这些命令主要用于调试和验证 SQL 脚本中的变量替换情况。 一、参数说明 1.1 SET VERIFY ON 作用&#xff1a;启用变量替换的验证功能。当启用时&#xff0c;S…