目录
不能被拷贝的类
要想设计一个不能被拷贝的类,首先需要知道哪些场景中会发生类的拷贝。拷贝只会发生在两个场景 :拷贝构造时和赋值时,因此想要让一个类不能被拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
在C++98 的标准中,需要把拷贝构造函数和赋值重载函数只声明不定义,且设置为私有,此时该类就无法调用拷贝构造函数和赋值重载函数了。
cpp
class NoCopy
{
public:
NoCopy()
{}
~NoCopy()
{}
private:
//拷贝构造和赋值重载只声明不定义,且设置为私有的
NoCopy(const NoCopy& nc);
NoCopy& operator=(const NoCopy& nc);
};
int main()
{
NoCopy n1;
NoCopy n2(n1);//调用拷贝构造
NoCopy n3;
n3 = n1;//调研赋值重载
return 0;
}
上述代码中,定义了一个NoCopy类,该类的拷贝构造和赋值重载只声明不定义,且设置为私有的。然后再main函数中尝试调用NoCopy类的拷贝构造和赋值重载。尝试编译运行一下:

可见把类的拷贝构造和赋值重载只声明不定义,且设置为私有的这种做法确实可以让一个类无法被拷贝。
注意:只声明不定义和设置为私有,两个条件缺一不可。
- 设置为私有:如果没有设置成private,那么用户就可以在类外定义了,就不能禁止拷贝了。
- 只声明不定义:不定义是因为该函数根本不会调用,定义了也没有什么意义,不写反而省事了,而且如果定义了,就可能会在成员函数内部进行拷贝了。
C++11 中扩展了delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上'=delete' ,表示让编译器强制删除掉该默认成员函数。
此时再想设计一个不能被拷贝的类,只需要把该类的拷贝构造函数和赋值重载函数使用delete强删除即可,且即使设置为公有的也不影响。
cpp
class NoCopy
{
public:
NoCopy()
{}
~NoCopy()
{}
//使用delete强制删除拷贝构造函数和赋值重载函数
NoCopy(const NoCopy& nc) = delete;
NoCopy& operator=(const NoCopy& nc) = delete;
};
int main()
{
NoCopy n1;
NoCopy n2(n1);//调用拷贝构造
NoCopy n3;
n3 = n1;//调研赋值重载
return 0;
}
上边的代码中,NoCopy类的拷贝构造函数和赋值重载函数被使用delete强制删除了,且是在public区域中,在main函数中尝试调用拷贝构造函数和赋值重载函数。尝试编译运行:

可见把类的拷贝构造函数和赋值重载函数使用delete强制删除,是可以让一个类无法被拷贝的。
只能在堆上创建对象的类
构造函数私有化:
创建一个类对象,只能创建在两个位置,要么是创建在栈上,要么是创建在堆上。而要创建一个类对象,必会调用构造函数或拷贝构造函数。而想要在堆上创建对象,就必须通过new/malloc等实现。因此要想实现只能在堆上创建对象,首先要将构造函数私有化,同时拷贝构造和赋值重载需要强制删除(或者只声明不定义且设置为私有 ),提供一个static成员函数,在该函数中完成对象的创建并返回对象指针。
cpp
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
//通过类成员函数返回对象
/*HeapOnly test()
{
return *this;
}*/
private:
//构造函数
HeapOnly() {}
//拷贝构造和赋值重载
//C++98:只声明不定义,且设置为私有
//HeapOnly(const HeapOnly&);
//HeapOnly& operator=(const HeapOnly& h);
//C++11:使用delete强制删除
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly& h) = delete;
};
int main()
{
HeapOnly* hp1 = HeapOnly::CreateObject();
//如果不把拷贝构造函数和赋值重载函数删除,就可以通过下边的方法在栈上创建对象
/*HeapOnly hp2(*hp1);
HeapOnly* hp3 = HeapOnly::CreateObject();
HeapOnly hp4(*hp3);
hp4 = *hp1;
HeapOnly hp5 = hp3->test();*/
delete hp1;
return 0;
}
上边的代码实现了一个只能在堆上创建对象的类HeapOnly,到那时还存在一个问题,CreateObject()方法返回的是一个原生指针 ,HeapOnly类虽然禁止了类对象的拷贝和赋值,但是原生指针之间还是可以互相赋值的,这就会导致多个指针指向同一份资源,释放资源时就会出问题。
cpp
int main()
{
HeapOnly* hp1 = HeapOnly::CreateObject();
HeapOnly* hp2 = hp1;
delete hp1;
delete hp2;
return 0;
}
上边的代码先通过CreateObject()方法创建了一个对象并把对象指针返回给后hp1,然后又把hp1赋值给hp2,编译运行代码:

可以发现程序运行崩溃了,就是以为重复释放了同一份资源导致的。
要解决这个问题就需要用到以前说过的智能指针了,通过智能指针可以更加安全有效的管理资源。
cpp
class HeapOnly
{
public:
static shared_ptr<HeapOnly> CreateObject()
{
return shared_ptr<HeapOnly>(new HeapOnly);
}
/*static HeapOnly* CreateObject()
{
return new HeapOnly;
}*/
private:
//构造函数
HeapOnly() {}
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly& h) = delete;
};
int main()
{
shared_ptr<HeapOnly> hp1 = HeapOnly::CreateObject();
shared_ptr<HeapOnly> hp2 = hp1;
return 0;
}
上述代码通过修改CreateObject()函数的返回值,实现了对资源的安全管理,可以正常编译运行。
shared_ptr可以让多个对象共享同一块资源,而unique_ptr是独占资源,可以根据不同的场景灵活使用。
需要注意的是CreateObject()函数在返回对象时不能使用make_shared<HeapOnly>():
make_shared<HeapOnly>():
make_shared 需要直接调用 HeapOnly的构造函数。
但 make_shared的模板实例化代码位于标准库中(
<memory>
头文件),不在 HeapOnly类的成员函数作用域内。因此,make_shared无法访问 HeapOnly的私有构造函数,导致编译错误。
要想使用make_shared,需要在HeapOnly类中声明make_shared为友元:
cppclass HeapOnly { public: template <typename T, typename... Args> friend shared_ptr<T> std::make_shared(Args&&... args); // 其他代码... };
shared_ptr(new HeapOnly):
- new HeapOnly的调用发生在 CreateObject()成员函数内部。
- 成员函数可以访问类的私有成员(包括私有构造函数)。
- 因此,new HeapOnly合法,shared_ptr可以安全接管指针。
析构函数私有化:
上边通过构造函数私有化实现了一个只能在堆上创建的类,此外还可以通过将析构函数私有化实现一个只能在堆上创建的类。
cpp
class HeapOnly
{
public:
//构造函数
HeapOnly() {}
void del()
{
delete this;
}
private:
//析构函数
~HeapOnly()
{}
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly& h) = delete;
};
int main()
{
HeapOnly* hp1 = new HeapOnly;
hp1->del();
HeapOnly hp2;
return 0;
}
上述代码中定义了一个HeapOnly类,该类将析构函数进行了私有化处理,在main函数中通过new在堆上创建一个对象,又直接在栈上定义了一个对象,编译运行:

编译直接报错,这是因为编译器在编译阶段会检查对象的 完整生命周期,包括析构函数的可访问性,即使构造函数是公有的,如果析构函数不可访问,编译器会直接拒绝栈对象的定义。
而new操作符仅依赖构造函数的可访问性 ,与析构函数无关。堆对象的销毁必须通过显式调用del()函数,不能直接使用delete操作符,因为delete操作符会尝试调用析构函数,而析构函数是私有的,无法在外部直接调用;而del()是类的成员函数,可以直接访问私有的析构函数。
只能在栈上创建对象的类
有了只能在堆上创建对象的经验,要设计一个只能在栈上创建对象的类就很简单了。要想在堆上创建对象,必须要调用new操作符,当使用new动态分配对象时,编译器会先调用operator new分配内存,再调用构造函数 。因此只需要把operator new强制删除,就可以禁止在堆上创建对象了。
cpp
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();
}
StackOnly(){}
// 禁用 new 操作符
void* operator new(size_t) = delete;
void operator delete(void*) = delete;
// 禁用 new[] 和 delete[]
void* operator new[](size_t) = delete;
void operator delete[](void*) = delete;
private:
};
int main()
{
StackOnly so1=StackOnly::CreateObj();
StackOnly so2;
//StackOnly* so3 = new StackOnly;//报错
return 0;
}
不能被继承的类
在C++的继承体系中,子类的构造函数 必须调用父类的构造函数初始化父类的那一部分成员,如果父类没有默认的构造函 数,则必须在子类构造函数的初始化列表阶段显示调用 。因此如果把父类的构造函数设置为私有的,那么在子类构造函数中将无法调用父类的构造函数,这就实现了父类不可被继承。
cpp
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit(){}
};
class A:public NonInherit
{
public:
A(){}
};
int main()
{
A a;
return 0;
}
上述代码中定义了一个NoInherit类,NoInherit类的构造函数设置为了私有的,有一个A类继承自NoInherit类,然后再main函数中定义A类对象,编译运行:

可以发现A类将无法定义对象,也就实现了NoInherit类不可被继承,因为只要继承了NoInherit类的子类都无法定义对象。
除了上述方法,C++11引入了final关键字来修饰类,表示该类不可被继承。
cpp
class NonInherit final
{
public:
NonInherit(){}
};
只能创建一个对象的类(单例模式)
设计模式:
设计模式是解决软件设计中常见问题的可复用方案,它们可以分为三大类:创建型、结构型和行为型。
创建型模式:控制对象的创建过程,解耦对象的创建与使用,提高灵活性和可维护性。主要包括:单例模式、工厂模式等。
结构型模式 : 组织类和对象的结构,通过组合或继承实现更灵活的设计。主要包括:适配器模式、装饰器模式等。
行为型模式:管理对象间的交互和职责分配,优化通信流程。主要包括:观察者模式、命令模式等。
实用设计模式的目的:提高代码可重用性 、让代码更容易被他人理解 、保证代码可靠性。使代码编写真正工程化。
单例模式:
一个类只能创建一个对象,这种模式叫做单例模式。单例模式确保一个类只有一个实例 ,并提供全局访问点 ,该实例被所有程序模块共享,需要通过静态对象实现。主要应用场景:数据库连接池、全局配置管理器。
单例模式有两种实现模式:饿汉模式和懒汉模式。
饿汉模式:
饿汉模式的设计思想是不管是否使用 ,在程序启动时(main函数之前)就直接创建一个唯一的静态实例对象。
优点:
- 简单,没有线程安全的问题。
缺点:
- 如果单例对象数据较多,构造初始化成本较高,那么会影响程序启动的速度。迟迟进不了main函数。
- 如果多个单例类有初始化启动依赖关系,饿汉模式无法控制。假设有A和B两个单例,B类依赖于A类,要求A先初始化,B再初始化,此时饿汉模式将无法保证初始化顺序。
cpp
//饿汉模式
namespace Hunger
{
//单例类
class Singleton
{
public:
static Singleton* GetInstance()
{
return &_sing;
}
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
private:
//将拷贝构造和赋值重载强制删除,防止通过它们破坏单例的唯一性
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
//构造函数私有化,防止随意创建对象
Singleton(int x, int y, const vector<string>& vstr)
:_x(x)
, _y(y)
, _vstr(vstr)
{}
int _x;
int _y;
vector<string> _vstr;
// 静态成员对象,不存在对象中,存在静态区,只有一份,相当于全局的,定义在类中,受类域限制
static Singleton _sing;
};
//类外定义静态成员
//只能在此处进行初始化设置数据
Singleton Singleton::_sing(1, 2, { "hello","haha" });
}
int main()
{
Hunger::Singleton* hs=Hunger::Singleton::GetInstance();
hs->Print();
hs->AddStr("nihao");
hs->Print();
return 0;
}
上述代码中实现了一个饿汉模式的单例类Hunger::Singleton,并在main函数中进行了相关调用,编译运行:
饿汉模式由于提前加载资源,适用于实例小且频繁使用的情况,可避免资源竞争,提高响应速度。
懒汉模式:
懒汉模式 的设计思想是在第一次使用实例对象时,创建对象。
优点:
- 第一次使用实例对象时,创建对象。程序启动无负载。多个单例实例启动顺序自由控制
缺点:
- 复杂,存在线程安全的问题。
线程不安全的懒汉模式:
cpp
namespace Lazy
{
class Singleton
{
public:
//获取单例对象
static Singleton* GetInstance()
{
//静态成员变量只会创建一次
if (_psing == nullptr)//在第一次调用时创建
{
_psing = new Singleton;
}
return _psing;
}
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
//删除单例对象
static void DelInstance()
{
std::cout << "static void DelInstance()" << std::endl;
if (_psing)
{
delete _psing;
_psing = nullptr;
}
}
~Singleton() {}
private:
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
//构造函数私有化,防止随意创建对象
Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "xxxxxxx" })
:_x(x)
, _y(y)
, _vstr(vstr)
{}
int _x;
int _y;
vector<string> _vstr;
// 静态成员对象,不存在对象中,存在静态区,相当于全局的,定义在类中,受类域限制
static Singleton* _psing;
//内部类,帮助删除单例对象
class GC
{
public:
~GC()
{
Singleton::DelInstance();
}
};
static GC _gc;
};
//静态成员类内声明,类外定义
Singleton* Singleton::_psing = nullptr;
Singleton::GC Singleton::_gc;
}
int main()
{
Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
ls->Print();
ls->AddStr("zaijian");
ls->Print();
//ls->DelInstance();手动调用
return 0;
}
上述代码中实现了一个基础的懒汉模式的单例类Lazy::Singleton,由于静态成员变量是一个指针,无法转动销毁对象资源,需要提供一个静态成员方法DelInstance(),用来释放对象资源,为防止忘记手动调用DelInstance()方法,设计一个内部类GC,专门用于调用DelInstance()方法释放对象资源。编译运行:

可以发现,DelInstance()方法自动调用了。
这种基础的懒汉模式的单例类,存在一个很大的问题,它是线程不安全 的,多线程下可能创建多个实例(竞态条件)。
单例模式的核心是确保一个类只有一个实例,并在全局提供访问点。在单线程环境下,因为GetInstance()会检查_psing是否为nullptr,如果是,则创建新实例,如果不是,则直接返回。但在多线程环境下,可能会有多个线程同时进入GetInstance() ,导致多个实例被创建,从而破坏单例的唯一性。
具体来说,线程不安全的原因在于没有同步机制。当两个或多个线程同时执行if (_psing==nullptr)时,它们都可能发现_psing为nullptr,从而各自执行_psing = new Singleton,导致多次实例化。
cpp
void test()
{
Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
std::cout << ls << std::endl;
}
int main()
{
/*Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
ls->Print();
ls->AddStr("zaijian");
ls->Print();*/
//ls->DelInstance();手动调用
vector<thread> threads;
// 启动多个线程
for (int i = 0; i < 10; ++i)
{
threads.emplace_back(test);
}
// 等待所有线程结束
for (auto& t : threads)
{
t.join();
}
return 0;
}
上边的代码中,test函数每次都会获取实例,并打印地址,main函数中,创建了10个线程都去执行test函数,观察现象:

可以发现,这是个线程打印的地址多数都是不同的,说明进行了多次实例化。
线程不安全的问题并不容易复现,因为线程的调度是操作系统控制的,可能在某些情况下测试通过,但在其他情况下失败。为了增加竞态发生的概率 ,可以在if条件判断后和实际创建实例前插入一些延迟 (this_thread::sleep_for(chrono::milliseconds(100))),模拟线程切换 的情况,以增加复现概率。
线程安全的懒汉模式:
为了保证线程安全,可以通过加锁的方法来实现。
cpp
namespace Lazy
{
class Singleton
{
public:
//获取单例对象
static Singleton* GetInstance()
{
//静态成员变量只会创建一次
//双检查加锁,保证线程安全
if (_psing == nullptr)
{
unique_lock<mutex> lock(_mutex);//加锁
if(_psing ==nullptr)//在第一次调用时创建
{
this_thread::sleep_for(chrono::milliseconds(100));//手动延时
_psing = new Singleton;
}
}
return _psing;
}
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
//删除单例对象
static void DelInstance()
{
std::cout << "static void DelInstance()" << std::endl;
if (_psing)
{
delete _psing;
_psing = nullptr;
}
}
~Singleton(){}
private:
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
//构造函数私有化,防止随意创建对象
Singleton(int x = 0, int y = 0, const vector<string>& vstr = {"xxxxxxx"})
:_x(x)
, _y(y)
, _vstr(vstr)
{}
int _x;
int _y;
vector<string> _vstr;
static mutex _mutex;//锁
// 静态成员对象,不存在对象中,存在静态区,相当于全局的,定义在类中,受类域限制
static Singleton* _psing;
//内部类,帮助调用单例类的析构
class GC
{
public:
~GC()
{
Singleton::DelInstance();
}
};
static GC _gc;
};
//静态成员类内声明,类外定义
Singleton* Singleton::_psing = nullptr;
mutex Singleton::_mutex;
Singleton::GC Singleton::_gc;
}
void test()
{
Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
std::cout << ls << std::endl;
}
int main()
{
vector<thread> threads;
// 启动多个线程
for (int i = 0; i < 10; ++i)
{
threads.emplace_back(test);
}
// 等待所有线程结束
for (auto& t : threads)
{
t.join();
}
return 0;
}
上边的代码通过加锁,保证了线程安全。

前边所说的懒汉模式的代码,都必须通过一个静态方法来释放对象资源,为防止忘记手动调用,还需要一个内部类GC来实现自动释放资源,因为单例对象是一个静态指针,无法自动释放资源,那可不可以直接使用静态对象来实现单例模式,这样就不用再单独考虑资源释放的问题了。
答案是可以的,但是需要在C++11之后才可以,因为C++11之前静态对象的创建无法保证线程安全,C++11之后保证了静态对象创建是线程安全的。
静态对象:
cpp
namespace Lazy
{
class Singleton
{
public:
//获取单例对象
static Singleton* GetInstance()
{
// 局部的静态对象,第一次调用函数时构造初始化
// C++11及之后这样写才可以
// C++11之前无法保证这里的构造初始化是线程安全的
this_thread::sleep_for(chrono::milliseconds(100));
static Singleton sint;
this_thread::sleep_for(chrono::milliseconds(100));
return &sint;
}
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
private:
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
//构造函数私有化,防止随意创建对象
Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "xxxxxxx" })
:_x(x)
, _y(y)
, _vstr(vstr)
{}
~Singleton()
{
cout << "~Singleton()" << endl;
}
int _x;
int _y;
vector<string> _vstr;
};
}
void test()
{
Lazy::Singleton* ls = Lazy::Singleton::GetInstance();
std::cout << ls << std::endl;
}
int main()
{
vector<thread> threads;
// 启动多个线程
for (int i = 0; i < 10; ++i)
{
threads.emplace_back(test);
}
// 等待所有线程结束
for (auto& t : threads)
{
t.join();
}
return 0;
}
上边的代码通过静态对象实现了一个线程安全的懒汉模式单例类。
