详解c++---特殊类设计

目录标题

  • 设计一个不能被拷贝的类
  • 设计一个只能从堆上创建对象的类
  • 设计一个只能在栈上创建对象的类
  • 设计一个无法被继承的类
  • 什么是单例模式
  • 饿汉模式
  • 饿汉模式的缺点
  • 懒汉模式
  • 懒汉模式的优点
  • 懒汉模式的缺点
  • 特殊的懒汉

设计一个不能被拷贝的类

拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。那么c++98采用的方式就是将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可,比如说下面的代码:

class CopyBan
{// ...private:CopyBan(const CopyBan&);CopyBan& operator=(const CopyBan&);//...
};

设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就不能禁止拷贝了,只声明不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数,那么这里的代码就如下:

class CopyBan
{// ...CopyBan(const CopyBan&)=delete;CopyBan& operator=(const CopyBan&)=delete;//...
};

设计一个只能从堆上创建对象的类

我们可以在三个位置上创建类的对象,分别为堆上栈上和静态区上,创建的方式如下:

int main()
{HeapOnly tmp1;//栈上HeapOnly* tmp2 = new HeapOnly;//堆上static HeapOnly tmp3;//静态区上
}

创建对象的时候必须得调用构造函数,那么这里就可以将构造函数放到私有里面,然后创建一个函数通过new来着堆上创建对象,但是这里会有一个先有鸡还是先有蛋的问题,所以我们把这个创建对象的函数变为静态的,比如说下面的代码:

class HeapOnly
{
public:static HeapOnly* CreateObject(){return new HeapOnly;}
private:HeapOnly() {}
};

这样写就可以让该容器智能在堆上创建空间,那么上面的代码运行的结果就如下:
在这里插入图片描述
可以看到上面的代码就出问题原因时无法调用构造函数,那么这里要想创建对象就智能调用里面的函数,比如说下面的代码

int main()
{HeapOnly* tmp = HeapOnly::CreateObject();
}

虽然上面的写法可以有效的避免在栈上和静态区上创建对象,但是它依然可以通过拷贝构造在栈上创建对象,所以还得把拷贝构造禁止掉,比如说下面的代码:

class HeapOnly
{
public:static HeapOnly* CreateObject(){return new HeapOnly;}
private:HeapOnly() {}HeapOnly(const HeapOnly&) = delete;
};

这里还有个方法就是将析构函数私有化,因为栈上的变量会自动销毁并调用析构函数,所以当对象的生命周期结束之后就会自动地调用析构函数来释放空间,但是析构函数私有化了所以掉不动,也就进一步阻止了对象在栈上船舰,但是堆上创建的对象也是掉不了析构函数,所以这里可以创建一个函数给堆上的变量能够调用析构函数来释放空间,那么这里地代码就如下:

#include<iostream>
using namespace std;
class HeapOnly
{
public:HeapOnly(){}void Destory(){this->~HeapOnly();}
private:~HeapOnly(){}HeapOnly(const HeapOnly& tmp) = delete;
};
int main()
{HeapOnly* tmp1 = new HeapOnly;tmp1->Destory();return 0;
}

运行地结果也是没有错误的
在这里插入图片描述

设计一个只能在栈上创建对象的类

这里也是跟上面的思路差不多,创建一个函数,函数中栈上创建对象并返回该对象,比如说下面的代码:

class StackOnly
{
public:static StackOnly creatobj(){return StackOnly();}
private:StackOnly(){}
};

那么我们就可以用下面的代码在栈上创建对象:

{StackOnly tmp1 = StackOnly::creatobj();return 0;
}

如果在其他位置上创建空间的话就会直接报错,比如说下面的代码:

int main()
{StackOnly* tmp1 = new StackOnly;static StackOnly tmp2;return 0;
}

报错的内容如下:
在这里插入图片描述
但是这种方法没有完全的封死,我们依然可以通过拷贝构造在静态区上开辟空间,比如说下面的代码:

int main()
{static StackOnly tmp1 = StackOnly::creatobj();return 0;
}

那能不能把拷贝构造函数也封掉的呢?答案使不行的,因为拷贝构造函数被封掉之后不仅静态区上无法创建空间,而且栈上也没有办法创建对象,这里还有种方法就是封掉operator new和operator delete,比如说下面的代码:

class StackOnly
{
public:StackOnly(){}void* operator new(size_t size) = delete;void operator delete(void* p) = delete;
private:
};

那么这个时候再使用new创建对象就会直接报错,比如说下面的运行结果:
在这里插入图片描述

但是这种方法只能保证不能在堆上开辟空间,无法保证在静态区上也不能开辟空间,比如说下面的代码还是可以正常运行的:

int main()
{static StackOnly tmp2;return 0;
}

所以痛过常规方法这里没有办法完全封死的,要想完全封死的话就只有一个办法就是封掉拷贝构造函数并且不接受对象,通过引用或者创建临时匿名对象来直接调用函数,比如说下面的代码:

int main()
{StackOnly::creatobj().Print();const StackOnly& so4 = StackOnly::creatobj();so4.Print();return 0;
}

代码的运行结果如下:
在这里插入图片描述
但是这样的实现存在一个缺陷就是无法修改对象里面的内容。

设计一个无法被继承的类

方法一就是构造函数私有, C++98中构造函数私有化,派生类中调不到基类的构造函数,所以无法继承

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

方法二就是添加final,final修饰类表示该类不能被继承,那么这里的代码就如下:

class NonInherit  final
{// ....
};

什么是单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理,单例模式有两种实现方式第一种就是饿汉模式,第二种就是懒汉模式,我们首先来看看饿汉模式的原理。

饿汉模式

单例模式的特点就是全局只有一个唯一对象,如何保证全局只有一个唯一对象呢?首先构造函数封死,如果构造函数不封起来的话使用者可以用这个类创建多个对象,那么我们就可以创建一个类,类中含有一个map容器并装着一些数据,然后类里面就提供了一些函数用于访问map的数据,修改map的数据等等,然后把这个类的构造函数放到私有里面,比如说下面的代码:

class InfoSingleton
{
public:private:InfoSingleton(){}map<string, int> _info;
};

然后这里就存在一个问题如何来创建对象,并且保证对象的个数就只有一个呢?答案是在类里面创建一个静态的变量然后提供一个静态成员函数来获取对象的引用,比如说下面的代码:

class InfoSingleton
{
public:static InfoSingleton& GetInstance(){return _sins;}
private:InfoSingleton(){}map<string, int> _info;static InfoSingleton  _sins;
};
InfoSingleton InfoSingleton::_sins;

外面的静态成员变量是定义,内部的静态成员变量是声明,然后为了方便往对象里面的插入数据和修改数据,我们还要创建一个insert函数,在里面通过方括号来修改内部的数据,比如说下面的代码:

void insert(string name, int salary)
{_info[name] = salary;
}

然后我们就可以在main函数里面通过引用或者匿名调用的方式来插入或者修改数据,比如说下面的代码:

int main()
{InfoSingleton::GetInstance().insert("张三", 10000);InfoSingleton::GetInstance().insert("李四", 12000);InfoSingleton& tmp = InfoSingleton::GetInstance();tmp.insert("王五", 13000);tmp.insert("老六", 14000);return 0;
}

然后我们还可以创建一个print函数用来打印内部的数据,比如说下面的代码:

void print()
{for (auto& ch : _info){cout << ch.first << ":" << ch.second << endl;}
}

然后我们就可以查看容器里面的数据,那么这里的运行结果如下:
在这里插入图片描述这种方式就是饿汉模式一开始就创建对象,但是这种方法存在一个问题就是拷贝构造和赋值重载会多创建出来一个对象不符合特征,所以这里得把拷贝构造和赋值重载也去掉,那么这里的代码就如下:

class InfoSingleton
{
public:static InfoSingleton& GetInstance(){return _sins;}void insert(string name, int salary){_info[name] = salary;}void print(){for (auto& ch : _info){cout << ch.first << ":" << ch.second << endl;}}
private:InfoSingleton(){}InfoSingleton(InfoSingleton const&) = delete;InfoSingleton& operator=(InfoSingleton const&) = delete;map<string, int> _info;static InfoSingleton  _sins;
};
InfoSingleton InfoSingleton::_sins;

我们就把这样的实现方式成为饿汉模式,因为它在程序的一开始就创建了一个对象。

饿汉模式的缺点

1.饿汉模式初始化时如果数据太多,会导致启动的速度较慢,因为饿汉模式在main函数之前就得进行初始化,而数据的含量可能会非常的多并且数据可能还要链接数据库等等,所以可能会导致启动的速度很慢。
2.多个单例类有初始化依赖关系,饿汉模式无法控制。比如说A和B都是单利类,要求先初始化A,再初始化B,因为B会依赖A,但是饿汉模式中的变量都是全局变量无法保证初始化顺序,所以这里就可能会出错,那么未来解决这个问题有人就提出了懒汉模式。

懒汉模式

饿汉模式是程序一开始就创建对象而懒汉模式则是先不着急创建对象,等需要的时候再创建对象,那么我们就把类里面的静态对象修改成为一个静态的指针对象,在类外面将其初始化为空,在GetInstance函数里面就判断当前的指针是否为空,如果为空的话就创建对象最后返回当前的指针指向的对象,那么这里的代码就如下:

class InfoSingleton
{
public:static InfoSingleton& GetInstance(){//第一次调用的时候创建对象if (_psins == nullptr){_psins = new InfoSingleton;}return *_psins;}void insert(string name, int salary){_info[name] = salary;}void print(){for (auto& ch : _info){cout << ch.first << ":" << ch.second << endl;}}
private:InfoSingleton(){}InfoSingleton(InfoSingleton const&) = delete;InfoSingleton& operator=(InfoSingleton const&) = delete;map<string, int> _info;static InfoSingleton*  _psins;
};
InfoSingleton* InfoSingleton::_psins=nullptr;

那么这里就不是一开始就创建对象,而是等你调用的时候创建对象。

懒汉模式的优点

1.对象在main函数之后才会创建,不会影响启动顺序。
2.可以主动控制初始化顺序。比如说A依赖于B,那么我们就可以先调用A再调用B来解决这个问题。

懒汉模式的缺点

多个线程一起调用单例对象的时候创建对象时也可能会创建多个对象,第一个线程看到的当前类没有对象会new一个,第一个线程还没有创建完成第二个线程跑过来发现依然没有所以这个时候就又会创建一个对象出来,所以这个时候就得在类里面添加一个枷锁变量,因为静态的成员函数没有this指针不能访问非静态的成员变量,所以这里就得创建一个静态的枷锁,那么这里的代码如下:

class InfoSingleton
{
public:static InfoSingleton& GetInstance(){mtx.lock();if (_psins == nullptr){_psins = new InfoSingleton;}mtx.unlock();return *_psins;}void insert(string name, int salary){}void print(){}
private:InfoSingleton(){}InfoSingleton(InfoSingleton const&) = delete;InfoSingleton& operator=(InfoSingleton const&) = delete;map<string, int> _info;static InfoSingleton*  _psins;static mutex mtx;
};
mutex InfoSingleton::mtx;
InfoSingleton* InfoSingleton::_psins=nullptr;

可是这里就存在一个问题,我们只会在第一次创建对象的时候出现问题,而我们每次使用这个函数的时候都得进行枷锁解锁,这里是不是就会产生时间消耗啊,所以能不能把枷锁放到if的里面呢?答案是不行的这里会出现线程安全,因为可能会两个线程都进入到创建对象里面,并且这种方法会加剧线程安全的风险,所以这里就可以添加双层检查来进行枷锁保护,那么这里的代码就如下:

static InfoSingleton& GetInstance()
{if (_psins == nullptr)//对象new出来了,避免每次都枷锁的检查,提高性能。{mtx.lock();if (_psins == nullptr)//保证线程安全且执行一次{_psins = new InfoSingleton;}mtx.unlock();}return *_psins;
}

但是这里new可能会抛出异常所以这里得捕捉异常,如果没捕捉的话这里就不会解锁了,那么改进后的代码就如下:

static InfoSingleton& GetInstance(){if (_psins == nullptr){mtx.lock();try{if (_psins == nullptr){_psins = new InfoSingleton;}}catch (...){mtx.unlock();throw;}}return *_psins;}

上面的释放方式有点不好看,所以这里我们可以使用智能指针的思想来解决这里的问题,创建一个类类中含有一个锁的引用对象,那么这个类的构造函数就是对这个锁进行上锁,类的析构函数就是对这个类进行解锁,那么这里的代码就是这样:

template<class Lock>
class LockGuard
{
public:LockGuard(Lock& lk):_lk(lk){_lk.lock();}~LockGuard(){_lk.unlock();}private:Lock& _lk;
};

那么上面的函数我们就可以写成下面这样:

static InfoSingleton& GetInstance()
{if (_psins == nullptr){LockGuard<mutex> lock(mtx);if (_psins == nullptr){_psins = new InfoSingleton;}}return *_psins;
}

一般单例对象不需要考虑内存释放,因为单例对象一般都是在整个程序里面进行使用,但是单例对象在不用时必须得手动处理,让一些资源进行报错,所以这个时候就得提供一个delete函数来手动释放,那么这里的函数代码如下:

static void DelInstance()
{/*保存数据到文件...*/std::lock_guard<mutex> lock(mtx);if (_psins){delete _psins;_psins = nullptr;}
}

那这里能不能做到自动释放该类呢?答案时可以的,我们可以定义一个内部类并且该类的析构函数里面调用DelInstance,然后在外部类里面添加一个该类的静态对象,这样当单例的那个类被销毁时内部类的对象就会被销毁,内部类对象被销毁时就会调用它的析构函数,然后析构函数就调用GetInstance函数来释放,那么完整的代码就如下:

template<class Lock>
class LockGuard
{
public:LockGuard(Lock& lk):_lk(lk){_lk.lock();}~LockGuard(){_lk.unlock();}private:Lock& _lk;
};
class InfoSingleton
{
public:static InfoSingleton& GetInstance(){if (_psins == nullptr){LockGuard<mutex> lock(mtx);if (_psins == nullptr){_psins = new InfoSingleton;}}return *_psins;}void insert(string name, int salary){_info[name] = salary;}void print(){for (auto& ch : _info){cout << ch.first << ":" << ch.second << endl;}}static void DelInstance(){/*保存数据到文件...*/std::lock_guard<mutex> lock(mtx);if (_psins){delete _psins;_psins = nullptr;}}class GC{public:~GC(){if (_psins){cout << "~GC()" << endl;DelInstance();}}}; 	
private:InfoSingleton(){}InfoSingleton(InfoSingleton const&) = delete;InfoSingleton& operator=(InfoSingleton const&) = delete;map<string, int> _info;static InfoSingleton*  _psins;static mutex mtx;static GC _gc;  
};
mutex InfoSingleton::mtx;
InfoSingleton* InfoSingleton::_psins=nullptr;
InfoSingleton::GC InfoSingleton::_gc;

特殊的懒汉

class InfoSingleton
{
public:static InfoSingleton& GetInstance(){static InfoSingleton sinst;return sinst;}void Insert(string name, int salary){_info[name] = salary;}void Print(){for (auto kv : _info){cout << kv.first << ":" << kv.second << endl;}cout << endl;}private:InfoSingleton(){cout << "InfoSingleton()" << endl;}InfoSingleton(const InfoSingleton& info) = delete;InfoSingleton& operator=(const InfoSingleton& info) = delete;map<string, int> _info;// ...
};

我们之前说过函数中的静态成员变量是在第一次调用该函数的时候进行创建并且初始化,第一次调用完之后就不会再执行该代码,所以上面的代码能够保证只创建一个对象,并且该对象的创建也发生了main函数之后,可是面对多线程的时候上面的代码可能会出现线程安全问题吗?答案是C++11之前,这里是不能保证sinst的初始化是线程安全的,C++11之后可以保证安全。所以这种写法不一定安全并不是通用的方法,所以对于这种写法如果编译器支持c++11则可以写,如果不支持则不能写。

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

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

相关文章

(Linux)基础命令

帮助文档 公式功能man 命令名访问Linux手册页命令名 – helpinfo 命令名查看命令的功能&#xff0c;来源&#xff0c;选项等whatis 命令名 ls 公式功能ls [选项][目录或文件]对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及…

自学网络安全究竟该从何学起?

一、为什么选择网络安全&#xff1f; 这几年随着我国《国家网络空间安全战略》《网络安全法》《网络安全等级保护2.0》等一系列政策/法规/标准的持续落地&#xff0c;网络安全行业地位、薪资随之水涨船高。 未来3-5年&#xff0c;是安全行业的黄金发展期&#xff0c;提前踏入行…

webrtc源码阅读之视频RTP接收JitterBuffer

在音视频通信中&#xff0c;网络抖动和延迟是常见的问题&#xff0c;会导致音视频质量下降和用户体验不佳。为了解决这些问题&#xff0c;WebRTC引入了Jitter Buffer&#xff08;抖动缓冲区&#xff09;这一重要组件。Jitter Buffer是一个缓冲区&#xff0c;用于接收和处理网络…

未来驾驶新标配;CarLuncher车载开发塑造智能娱乐导航系统

车载开发在新能源汽车的快速市场占有率增长背景下具有广阔的前景。随着环境保护意识的增强和政府对清洁能源的支持&#xff0c;新能源汽车&#xff08;如电动汽车&#xff09;在全球范围内呈现出快速增长的趋势。这种趋势为车载开发提供了许多机会和潜在市场。 新能源汽车的普…

基于SpringBoot+微信小程序的医院预约叫号小程序

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 该项目是基于uniappWe…

【C++初阶】构造函数和析构函数

文章目录 一、类的六个默认成员函数二、构造函数三、析构函数 一、类的六个默认成员函数 &#x1f4d6;默认成员函数 用户没有显式实现&#xff0c;编译器会自动生成的成员函数&#xff0c;称为默认成员函数。 构造函数&#xff1a;完成对象的初始化工作。析构函数&#xff…

【http-server】http-server的安装、前端使用http-server启动本地dist文件服务:

文章目录 一、http-server 简介:二、安装node.js:[https://nodejs.org/en](https://nodejs.org/en)三、安装http-server:[https://www.npmjs.com/package/http-server](https://www.npmjs.com/package/http-server)四、开启服务&#xff1a;五、http-server参数&#xff1a;【1…

Vscode配置grpc+c#+proto

首先是环境配置&#xff0c;用的dotnet5.0的sdk&#xff0c;所以Vscode的C#插件版本要选择1.24&#xff0c;然后需要配置C# Snippets、NuGget Package Manager、vscode-proto3、vscode-solution-extension&#xff08;可选&#xff09;。 以vscode-solution-extension为例新建A…

在 Linux 系统上下载 Android SDK

使用ubuntu系统进行车机开发&#xff0c;今天开始配置环境&#xff0c;首先是下载android studio&#xff0c;然后下载android sdk&#xff0c;这里需要注意的是linux系统不能使用windows系统下的Android sdk&#xff0c;亲测会出现各种问题。 常规思路&#xff0c;下载sdk&am…

Jenkins的几种安装方式以及邮件配置

目录 Jenkins介绍 Jenkins下载、安装 一、通过war包安装 二、通过docker安装 jenkins 容器中添加 git, maven 等组件 jenkins 容器中的公钥私钥 在 jenkins 容器中调用 docker 简单的方式启动 Docker server REST API 一个 jenkins 示例 三、通过Homebrew安装 访问Je…

oceanbase基础

与mysql对比 分布式一致性算法 paxos 存储结构&#xff08;引擎&#xff09;用的是两级的 数据库自动分片功能&#xff0c;提供独立的obproxy路由写入查询等操作到对应的分片 多租户 方便扩展 存储层 http://www.hzhcontrols.com/new-1391864.html LSM tree&#xff0c;is very…

【cfengDB】自己实现数据库第0节 ---整体介绍及事务管理层实现

LearnProj 内容管理 MySQL系统结构一条SQL执行流程 cfengDB整体结构事务管理TM模块TID文件规则定义文件读写 -- NIORandomAccessFile、FileChannel、ByteBuffer接口实现文件合法检测begin()commit(tid)rollback(tid)tid文件创建 本文作为数工底层的项目CfengDB开始篇章&#xf…

UART串口通信协议

一、串行通信 串行通信分为两种方式&#xff1a;同步串行通信和异步串行通信。 同步串行通信需要通信双方在同一时钟的控制下&#xff0c;同步传输数据。 异步串行通信是指通信双方使用各自的时钟控制数据的发送和接收过程。 二、UART 通用异步收发传输器&#xff08;Unive…

【Vue/element】 el-table实现表格动态新增/插入/删除 表格行,可编辑单元格

el-table实现表格动态新增/插入/删除 表格行&#xff0c;可编辑单元格 效果如下&#xff1a; 点击“新增一行”可以在表格最后新增一行&#xff0c;单元格内容可编辑 点击绿色按钮&#xff0c;可在指定行的后面插入一行 点击红色-按钮&#xff0c;可以删除指定行 原理&#…

让小程序动起来-轮播图的两种方式--【浅入深出系列003】

浅入深出系列总目录在000集 如何0元学微信小程序–【浅入深出系列000】 文章目录 本系列校训学习资源的选择啥是轮播图轮播图的关键代码最常见的轮播图代码便于理解的轮播代码两种轮播代码的比较 实际操练第一步&#xff0c;就是找到文件。第二步&#xff0c;先改动一下最显眼…

Docker使用总结

Docker 1.什么是 Docker 官网的介绍是“Docker is the world’s leading software container platform.” 官方给Docker的定位是一个应用容器平台。 Docker 是一个容器平台的领导者 Docker 容器平台 Docker 应用容器平台 application项目 Mysql Redis MongoDB ElasticSeacrh …

分布式运用——存储系统Ceph

分布式运用——存储系统Ceph 一、Ceph 介绍1.Ceph 简介2、存储基础2.1 单机存储设备2.2 单机存储的问题2.3 商业存储解决方案2.4 分布式存储&#xff08;软件定义的存储 SDS&#xff09;2.5 分布式存储的类型 3.Ceph 优势3.1 高扩展性3.2 高可靠性3.3 高性能3.4 功能强大 4.Cep…

hybridCLR热更遇到问题

报错1&#xff1a; No ‘git‘ executable was found. Please install Git on your system then restart 下载Git安装&#xff1a; Git - Downloading Package 配置&#xff1a;https://blog.csdn.net/baidu_38246836/article/details/106812067 重启电脑 unity&#xff1a;…

嵌入式工程师常用的软件工具推荐

前言&#xff1a;常言道&#xff1a;工欲善其事&#xff0c;必先利其器。作为一名合格的嵌入式工程师&#xff0c;日常可能需要接触和处理各种奇奇怪怪的问题&#xff0c;这时候一款高适配性的工具将会令工作效率大大提升。作者根据个人的实际使用情况与粉丝的客观感受&#xf…

MySQL表的约束

目录 前言 1.什么是约束 2.空属性 3.默认值 4.列描述 5.zerofill 6.主键 7.自增长 8.唯一键 9.外键 总结 前言 hello&#xff0c;各位小伙伴大家好&#xff0c;本章内容为大家介绍关于MySQL约束的相关内容&#xff0c;关于约束这个概念&#xff0c;如果是第一次接触可…