拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98的做法是:将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
};
原因:
C++11:扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
class CopyBan
{
// ...
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
//...
};
第一种方法是:将析构函数私有,这样保证了只能在堆上创建,而局部和静态变量无法创建,因为它们在销毁时会自动调用析构,私有后无法调用析构,但是当delete堆上创建的对象时也会报错,因为会先去调用它的析构,因此需要专门写一个释放函数,防止内存泄漏。
class HeapOnly {
public:
HeapOnly() {}
void del() {
delete this;
}
private:
~HeapOnly() {}
HeapOnly(const HeapOnly&);
HeapOnly& operator=(const HeapOnly&);
};
int main() {
//下面两种定义报错
/*HeapOnly hp1;
static HeapOnly hp2;*/
HeapOnly* hp3 = new HeapOnly;
hp3->del();
return 0;
}
第二种方法是:将构造函数私有,然后公有出一个返回堆上对象指针的静态成员函数即可。
class HeapOnly {
public:
static HeapOnly* createrObj() {
return new HeapOnly;
}
private:
HeapOnly() {}
HeapOnly(const HeapOnly&);
HeapOnly& operator=(const HeapOnly&);
};
int main() {
HeapOnly* hp3 = HeapOnly::createrObj();
return 0;
}
若不声明为静态成员函数会出现歧义,需要对象才能调用该函数,而就是因为没有对象才需要调用该函数创建对象,经典先有鸡还是先有蛋的问题,因此必须声明为静态的,这样无需对象,通过类名可以直接调用。
注意:不管哪种方法都需要把两个拷贝函数私有或者delete,这样完全只能在堆上创建对象,避免了通过拷贝而在栈上创建对象。
做法与上面类似,也是将构造函数私有,然后公有出一个静态成员函数,该函数内部创建一个对象然后返回。
还有第一点,禁了构造但没有禁拷贝构造,因此new可以调用拷贝构造创建一个对象,而又不能直接将拷贝构造声明为删除函数,因为传值返回或者外部会用到它创建对象,那该怎么办呢?
由于new是由两部分组成,operator new + 构造(拷贝构造),其中operator new是一个全局函数,可以在类中对它重载一个该类专属的operator new,若重载了,new该类对象时就不会去调用全局,而是去调用类中的,既然在类中又可以将其声明为删除函数,这样new就无法调用operator new了,进而无法在堆上创建对象了。
class StackOnly {
public:
static StackOnly createrObj() {
return StackOnly();
}
private:
StackOnly() {}
void* operator new(size_t size) = delete;
};
int main() {
StackOnly hp3 = StackOnly::createrObj();;
return 0;
}
C++98方式:构造函数私有化,派生类中调不到基类的构造函数。则无法继承,因为语法规定,派生类中基类的成员必须要调用基类的构造函数进行初始化,而把构造函数私有了之后对于派生类而言是不可见的,这样就完成了基类无法被继承。
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
C++11方式:在类名的后面加上final关键字,表示该类无法被继承。
class A final
{
// ....
};
设计模式:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式分为两种:
该模式的特点是,不管你后续用不用,在一开始(main函数之前)就创建一个唯一的全局实例对象。
模拟实现:
class Singleton {
public:
//2. 提供一个获取单例对象的接口
static Singleton& GetInstance()
{
return m_instance;
}
void add(const pair<string, string>& kv) {
_dict[kv.first] = kv.second;
}
void print() {
for (auto& kv : _dict) {
cout << kv.first << ':' << kv.second << endl;
}
}
private:
//1. 构造函数私有
Singleton() {};
//3. 将拷贝函数删除,防拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
map<string, string> _dict;
//静态成员变量类中声明
//类外定义
static Singleton m_instance;
//类中可以定义自己类型的对象吗?
//可以,但必须是静态的
//因为静态成员变量只会初始化一次存储在静态区
//不存储在任何一个对象中,
//且整个类的所有对象共享这一个成员
//若不是静态的就会出现循环问题:
//m_instance对象中有一个m_instance成员
//m_instance成员本身是个对象,该对象中有一个m_instance成员
//...
};
Singleton Singleton::m_instance;
//可以用全局变量吗?
//不行,因为构造函数私有,外部无法调用。
int main() {
Singleton::GetInstance().add({ "xxx", "111" });
Singleton::GetInstance().add({ "yyy", "222" });
Singleton::GetInstance().add({ "zzz", "333" });
Singleton::GetInstance().add({ "ddd", "444" });
Singleton::GetInstance().print();
return 0;
}
缺点:
针对上述的两个缺点,可以使用懒汉模式来解决。
它的特点是,在第一次使用的时候才初始化,若有多个实例且存在依赖,可以自由调整实例化顺序。
大体结构与饿汉的实现是相似的,模拟实现:
class Singleton {
public:
//2. 提供一个获取单例对象的接口
static Singleton& GetInstance()
{
//第一次调用才实例出单例对象
if (m_instance == nullptr) {
m_instance = new Singleton;
}
return *m_instance;
}
//与饿汉不同,它不存在释放问题
//而懒汉对象是new出来的,就需要考虑释放的问题
//但一般而言单例对象是不需要释放的
//因为进程结束,资源会自动回收
//但可能一些特殊情况,比如:
//1. 中途不会在使用了想要显式地释放
//2. 进程结束需要对象中的数据需要持久化保存(或者其它操作)
//因此是有必要写个释放逻辑的
static void DelInstance() {
//不为空就释放
if (m_instance) {
delete m_instance;
m_instance = nullptr;
}
}
void add(const pair<string, string>& kv) {
_dict[kv.first] = kv.second;
}
private:
//1. 构造函数私有
Singleton() {}
~Singleton() {
//持久化
FILE* fin = fopen("map.txt", "w");
for (auto& kv : _dict) {
fputs(kv.first.c_str(), fin);
fputs(": ", fin);
fputs(kv.first.c_str(), fin);
fputs("\n", fin);
}
//...或者其它操作
}
//3. 将拷贝函数删除,防拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
map<string, string> _dict;
//这里声明一个对象的指针
static Singleton* m_instance;
};
//在main函数之前仅仅只初始化一个指针
//相较于饿汉模式可以减少程序的启动时间
Singleton* Singleton::m_instance = nullptr;
int main() {
Singleton::GetInstance().add({ "xxx", "111" });
Singleton::GetInstance().add({ "yyy", "222" });
Singleton::GetInstance().add({ "zzz", "333" });
Singleton::GetInstance().add({ "ddd", "444" });
Singleton::GetInstance().print();
//显式释放
Singleton::DelInstance();
return 0;
}
还有一个问题:若进程结束的地方有很多要在每个地方都显式地写释放吗?
其实不必,有两种做法:
class GC {
public:
~GC() {
Singleton::DelInstance();
}
};
GC gc;
定义GC类和GC类的全局对象gc,当进程结束时,gc的生命周期也结束了,然后它会调用它的析构函数,函数内部刚好完成了对单例对象的释放工作。
若有多个单例对象需要释放时,也可以统一写在里面。
2.
class Singleton {
public:
//...
private:
//...
class GC {
//内部类
public:
~GC() {
Singleton::DelInstance();
}
};
//在内部定义
static GC _gc;
}
Singleton::GC Singleton::_gc;
在单例类内部声明GC类,然后定义一个静态GC类对象,这样当进程结束后会调用_gc的析构函数,效果与第一种方法是一样的。