设计一个类,不能被拷贝
拷贝存在两个场景:拷贝构造和赋值运算符重载,要让一个类禁止拷贝,只需要让该类不能调用构造函数和赋值运算符重载即可。
C++98的做法:
cpp
class CopyBan
{
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
};
- 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,还是可以拷贝。
- 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写 反而还简单,而且如果定义了,在成员函数还是能完成拷贝。
C++11的做法:
cpp
class CopyBan
{
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
};
C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上
=delete,表示让编译器删除掉该默认成员函数。
设计一个类,只能在堆上创建对象
- 将类的构造函数,和拷贝构造声明成私有(防止调用拷贝在栈上生成对象)。
- 提供一个静态成员函数,在该静态成员函数中完成堆对象的创建。
cpp
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
//将构造函数设计为私有
HeapOnly()
{
}
//C++98防拷贝:
// 1、声明成私有
HeapOnly(const HeapOnly&)
{
}
// 2、只声明不实现
HeapOnly(const HeapOnly&);
//C++11:声明成delete
HeapOnly(const HeapOnly&) = delete;
};
int main()
{
HeapOnly hp;//error
HeapOnly* hp1 = new HeapOnly;//error new也会调用构造函数初始化
HeapOnly hp2(*hp1);//error
HeapOnly* hp3 = HeapOnly::CreateObject();//只能通过静态成员函数在堆上创建对象
return 0;
}
设计一个类,只能在栈上创建对象
cpp
class StackOnly
{
public:
static StackOnly CreateObject()
{
return StackOnly();
}
private:
//将构造函数设计为私有
StackOnly()
{
}
};
int main()
{
StackOnly so = StackOnly::CreateObject();
StackOnly* p = new StackOnly;//error
return 0;
}
如果将operator new直接删除呢?
cpp
class StackOnly
{
public:
void* operator new (size_t size) = delete;
};
int main()
{
StackOnly* p = new StackOnly;//error
//存在缺陷:无法阻止在数据段(静态区)创建对象
static StackOnly so;//创建的对象在静态区
return 0;
}
设计一个类,不能被继承
C++98的做法:
cpp
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
class B :public NonInherit
{
};
int main()
{
B b;//error "无法引用"B"的默认构造函数
}
C++11的做法:
cpp
//final关键字,final修饰类,表示该类不能被继承。
class NonInherit final
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
class B :public NonInherit //error "不能将"final"类类型用作基类
{
};
int main()
{
B b;//error "无法引用"B"的默认构造函数"
}
常见的设计模式
- 迭代器模式:基于面向对象三大特性之一的封装设计出来的,用迭代器类封装以后,可以在不暴露容器的结构情况下,可以使用统一的方式访问容器中的数据。
- 适配器模式:体现的是一种复用,例如栈和队列,复用了双端队列接口。
- 还有一些常见的设计模式:工厂模式、装饰器模式、观察者模式、单例模式......
单例模式
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个****访问它的全局访问点,该实例被所有程序模块共享。
- 设计一个类,只能创建一个对象
cpp
//单例模式
//一个类只能创建一个对象
class Singleton
{
public:
static Singleton* GetInstanc()
{
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
return _pinst;
}
private:
//构造函数设计为私有
Singleton()
{
}
//删除拷贝构造和赋值运算符重载
Singleton(const Singleton& s) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton* _pinst;
};
Singleton* Singleton::_pinst = nullptr;
int main()
{
cout << Singleton::GetInstanc() << endl;
cout << Singleton::GetInstanc() << endl;
cout << Singleton::GetInstanc() << endl;
//Singleton copy(*Singleton::GetInstanc());//将拷贝函数私有
return 0;
}
设计思路:
- 将类的构造函数设计为私有,类外无法通过new创建对象。
- 将拷贝函数删除,若不删除,类外可以通过已有的对象创建新的对象。
- 使用静态成员变量_pinst,存储单例对象的唯一实例。静态成员属于类本身,而不是类的某个对象。因此,所有对象共享同一个静态成员。在整个程序运行期间,_pinst只会存在一份,用于指向唯一的Singleton对象。
单例模式保证一个类在整个程序运行期间只有一个实例。
总结:
通过将构造函数私有化、使用静态成员指针存储唯一实例、提供静态成员函数获取实例以及删除拷贝构造函数,确保了Singleton类在整个程序运行期间只能创建一个对象,并且提供了一个全局访问点来获取这个唯一的对象。
但是现在设计的单例模式在多线程环境下还是有问题的。
cpp
//测试此单例模式在多线程环境下出现的问题
#include<iostream>
#include<thread>
#include<vector>
#include<windows.h>
class Singleton
{
public:
static Singleton* GetInstanc()
{
::Sleep(500);
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
return _pinst;
}
private:
//构造函数设计为私有
Singleton()
{
}
//删除拷贝构造和赋值运算符重载
Singleton(const Singleton& s) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton* _pinst;
};
Singleton* Singleton::_pinst = nullptr;
int main()
{
vector<std::thread> vthreads;
int n = 4;
for (int i = 0; i < n; i++)
{
vthreads.push_back(std::thread([]()
{
cout << Singleton::GetInstanc() << endl;
}));
}
for (auto& t : vthreads)
{
t.join();
}
return 0;
}

多个线程是并发执行的,它们可能会几乎同时检查到 _pinst == nullptr 这个条件成立。例如,线程 A 和线程 B 都进入了 if ( _pinst == nullptr ) 这个判断语句内部。它们都会执行 _pinst = new Singleton 这行代码来创建Singleton对象。这样就会导致创建出多个Singleton类的实例。
加锁。这样有问题吗?
cpp
class Singleton
{
public:
static Singleton* GetInstanc()
{
//::Sleep(500);//增加没加锁时出现线程不安全的条件
_mtx.lock();
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
_mtx.unlock();
return _pinst;
}
private:
//构造函数设计为私有
Singleton()
{
}
//删除拷贝构造和赋值运算符重载
Singleton(const Singleton& s) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton* _pinst;
static mutex _mtx;
};
Singleton* Singleton::_pinst = nullptr;
mutex Singleton::_mtx;
如果if语句中 _pinst == nullptr;抛异常,例如申请内存失败,那么会导致 _mutex();这行代码不会被执行,锁没有被释放,其他线程进来之后也申请不了锁,陷入阻塞状态,最终造成死锁。
cpp
class Singleton
{
public:
static Singleton* GetInstanc()
{
//::Sleep(500);
// _mtx.lock();
//限定作用域,保证加锁的粒度更小,lock出了作用域生命周期就到了,析构自动释放锁
{
unique_lock<mutex> lock(_mtx);
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
}
//_mtx.unlock();
//...
return _pinst;
}
private:
//构造函数设计为私有
Singleton()
{
}
//删除拷贝构造和赋值运算符重载
Singleton(const Singleton& s) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton* _pinst;
static mutex _mtx;
};
使用 std::unique_lock ,此时不论是否抛异常,出来作用域,生命周期到了之后,会自动调用析构函数释放锁。
但是这样的设计,有缺陷,存在优化的空间,每次线程进来申请对象时,都要加锁,解锁,是不是浪费资源呢?我们只需要保证第一次多个线程同时进来申请对象时,加锁就可以了,当一个线程申请到锁之后,之后的线程再来申请对象,就不需要加锁了。
cpp
class Singleton
{
public:
static Singleton* GetInstanc()
{
//双重检查锁定
if (_pinst == nullptr) //后续线程再进来判断为假直接返回 不需要申请锁资源
{
// _mtx.lock();
{
//加锁保护临界区
unique_lock<mutex> lock(_mtx);
//再次检查,防止多个线程解锁之后都创建实例
if (_pinst == nullptr) //保证原子性
{
_pinst = new Singleton;
}
}
//_mtx.unlock();
}
//...
return _pinst;
}
private:
//构造函数设计为私有
Singleton()
{
}
//删除拷贝构造和赋值运算符重载
Singleton(const Singleton& s) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton* _pinst;
static mutex _mtx;
};
Singleton* Singleton::_pinst = nullptr;
mutex Singleton::_mtx;
int main()
{
vector<std::thread> vthreads;
int n = 4;
for (int i = 0; i < n; i++)
{
vthreads.push_back(std::thread([]()
{
cout << Singleton::GetInstanc() << endl;
}));
}
for (auto& t : vthreads)
{
t.join();
}
return 0;
}
当第一次多个线程同时判断if条件为真,申请锁时,只有一个线程能申请成功,其他线程会陷入阻塞,申请锁成功的线程再次判断if条件为真,创建实例,出了作用域,自动释放锁,此时,其他线程申请锁成功,判断if条件为假,出了作用域,自动释放锁,返回第一个线程创建的实例......所以第一次进来的多个线程,最终只有第一个线程创建出了单例实例。之后再进来的线程,在第一个if条件就不满足,不会再申请锁资源了。
由于单例对象的生命周期贯穿整个程序,在程序结束时,如果没有释放单例对象所占用的内存,就会造成内存泄漏。我们再来提供一个释放资源的接口。
懒汉模式
cpp
class Singleton
{
public:
static Singleton* GetInstanc()
{
//双重检查锁定
if (_pinst == nullptr)
{
// _mtx.lock();
{
//加锁保护临界区
unique_lock<mutex> lock(_mtx);
//再次检查,防止多个线程解锁之后都创建实例
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
}
//_mtx.unlock();
}
//...
return _pinst;
}
static void DeleteInstance()
{
//unique_lock<mutex> lock(_mtx);
delete _pinst;
_pinst = nullptr;
}
private:
//构造函数设计为私有
Singleton()
{
}
//删除拷贝构造和赋值运算符重载
Singleton(const Singleton& s) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton* _pinst;
static mutex _mtx;
};
Singleton* Singleton::_pinst = nullptr;
mutex Singleton::_mtx;
class GC
{
public:
~GC()
{
Singleton::DeleteInstance();
}
};
static GC gc;//静态对象的生命周期贯穿整个程序,程序结束时,自动调用gc的析构函数
int main()
{
vector<std::thread> vthreads;
int n = 4;
for (int i = 0; i < n; i++)
{
vthreads.push_back(std::thread([]()
{
cout << Singleton::GetInstanc() << endl;
}));
}
for (auto& t : vthreads)
{
t.join();
}
//...
//...
//手动释放单例对象
//Singleton::DeleteInstance();
//即使没有手动释放,静态对象gc析构,自动释放单例对象
return 0;
}
上面我们实现的单例模式为懒汉模式的单例模式。
饿汉模式
cpp
class Singleton
{
public:
static Singleton* GetInstance()
{
return &_inst;
}
private:
// 构造函数私有
Singleton() {};
// C++98 防拷贝
//Singleton(Singleton const&);
//Singleton& operator=(Singleton const&);
// C++11
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton _inst;
};
Singleton Singleton::_inst;//在程序启动之前就创建了
上面我们实现的方式为饿汉模式的单例模式。
懒汉模式的特点
- 延迟初始化:懒汉式单例模式在第一次使用单例对象时才进行实例化。也就是说,在程序启动时,单例对象并不会立即创建,而是等到第一次调用获取单例对象的方法时,才会去检查对象是否已经创建,如果没有创建就进行创建。
- 线程安全问题:在多线程环境下,如果不做特殊处理,懒汉式单例模式可能会出现多个线程同时创建单例对象的情况,破坏单例的唯一性。因此,通常需要使用锁机制来保证线程安全。
优点:
- 延迟加载:只有在真正需要使用单例对象时才进行创建,节省了系统资源。如果单例对象的创建过程比较复杂或者占用资源较多,且在某些情况下可能整个程序运行过程中都不会用到该单例对象,那么懒汉式可以避免不必要的资源浪费。
- 灵活性较高:可以根据运行时的条件来决定是否创建单例对象,比如根据配置文件的内容或者某些环境变量来判断是否需要创建特定的单例。
缺点:
- 线程安全问题
- 复杂度较高
饿汉模式的特点
- 立即初始化:饿汉式单例模式在程序启动时就会立即创建单例对象,无论后续是否会使用该对象。这意味着单例对象的创建是在程序加载时完成的,而不是在第一次使用时。
- 线程安全:由于饿汉式单例模式在程序启动时就已经创建了单例对象,不存在多线程同时创建对象的问题,所以天生是线程安全的,不需要额外的锁机制来保证线程安全。
优点:
- 线程安全
- 简单直观
缺点:
- 资源浪费:无论是否需要使用单例对象,在程序启动时都会创建,可能会导致一些资源的浪费。如果单例对象占用的资源较多,而在某些情况下程序可能并不会用到该对象,那么这种浪费可能会对系统性能产生一定的影响。
- 缺乏灵活性:一旦程序启动,单例对象就已经创建,无法根据运行时的条件来决定是否创建或者延迟创建,不够灵活。
总结:饿汉模式在程序启动时就创建对象,优点是天生就是线程安全的,实现简单,缺点是会造成资源的浪费,缺乏灵活性;懒汉模式在使用单例对象的时候才创建,优点是延迟加载,节省系统资源,灵活性较高,缺点是存在线程安全的问题,需要加锁保护,实现复杂度高。