目录
一、设计一个不能拷贝类
二、设计一个只能在堆上创建对象的类
方法一:析构函数私有化
方法二:构造函数私有化
三、设计一个只能在栈上创建对象的类
四、设计一个类不能被继承
五、设计一个只能创建一个对象的类(单例模式)
1.设计模式
2.单例模式
饿汉模式
懒汉模式
一、设计一个不能拷贝类
拷贝只会产生在两个场景中:调用拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
- C++98
将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan { // ... private: CopyBan(const CopyBan&); CopyBan& operator=(const CopyBan&); //... };
原因:
1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,还是能拷贝。
2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
- C++11
C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
class CopyBan { // ... CopyBan(const CopyBan&)=delete; CopyBan& operator=(const CopyBan&)=delete; //... };
二、设计一个只能在堆上创建对象的类
方法一:析构函数私有化
那么我们怎么创建对象呢?其实这里不能直接创建对象,只能用指针间接创建一个对象,如下所示。
可以直接用delete吗?
不行,因为delete释放对象的资源会调用对象的析构函数,而析构函数不可访问。这该怎么办呢?
既然析构函数被设置为私有,只能够在类内调用,那我们设置一个public函数调用析构函数可以吗?如下所示:
class HeapOnly
{
public:
void Destroy()
{
this->~HeapOnly();
}
private:
~HeapOnly()
{
cout << "~HeapOnly" << endl;
}
};
//方法一:析构函数私有化
//因为没有析构函数,就只能用new申请资源赋值给类类型的指针。
int main()
{
//HeapOnly hp1;
HeapOnly* hp2 = new HeapOnly;
//delete hp2;
hp2->Destroy();
return 0;
}
运行一下:
调用了析构函数没有问题。
我们还可以用delete调用析构函数,
但这种写法有些繁琐,我们可以将Destroy设置成静态的类成员函数。
这样调用就简单一点。
注意:类的静态成员函数不能使用非静态成员变量,因为使用非静态成员变量要this,而静态成员函数中没有this。上面的静态成员函数调用的变量是ptr,ptr不是成员变量,更不是非静态成员变量,不需要this调用。delete ptr,是delete调用ptr指向空间的析构函数。
方法二:构造函数私有化
因为构造函数私有了,类外部不能直接访问,所以不能直接实例化,也不能通过new赋值给类类型指针了。
同析构函数一样,我们也可以用间接调用构造函数的方式,创造对象指针,如下
但是这里报错 :CreateObj显示未定义,这是为什么呢?因为CreateObj是成员函数,调用成员函数要先创建对象或对象的指针,而我们又要用CreateObj创建对象。
怎么解决这个问题呢?我们可以使用静态成员函数,
这样就行解决了。注意:这里的new调用构造函数,构造函数不需要this调用。
但这种写法有个致命缺陷,就是可以通过拷贝构造创建栈上的变量。如下所示:
由于是浅拷贝,所以hp4和hp3指向同一块空间。我添加一个私有成员变量check_i来核验一下,
所以我们要把拷贝构造禁掉,
三、设计一个只能在栈上创建对象的类
因为只能在栈上创建对象,所以要限制对象的创建方式,我们先将构造函数设置为私有,这样就只能通过类内部的某个函数调用构造函数,在这个函数中,我们通过在栈上创建变量,然后返回该变量, 同时要将这个函数设置为静态的,因为非静态成员要用隐式的this调用。
//设计一个只能在栈中创建对象的类
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly obj;
return obj;
}
private:
StackOnly()
{
cout << "StackOnly()" << endl;
}
int check_i = 666;
};
int main()
{
StackOnly obj = StackOnly::CreateObj();
return 0;
}
运行,
这样只限制了构造函数,但可以通过new拷贝构造的对象申请堆上的空间,如下
那可以把拷贝构造delete吗?
会报错,因为传值返回会调用拷贝构造。
我们知道new分为两个步骤,一个是调用operator new ,一个是调用构造函数。
我们可以从operator new出手。对于每一个类,如果我们不显示实现类专属的operator new,它就会调用全局的operator new。如果我们实现类专属的operator new,如下
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly obj;
return obj;
}
//StackOnly(const StackOnly& obj) = delete;
void* operator new(size_t size)
{
cout << "void* operator new(size_t size)" << endl;
return malloc(size);
}
private:
StackOnly()
{
cout << "StackOnly()" << endl;
}
int check_i = 666;
};
int main()
{
StackOnly obj = StackOnly::CreateObj();
StackOnly* ptr = new StackOnly(obj);
return 0;
}
编译器就会优先调用显示实现的operator new,我们执行一下,
确实如此。
所以我们可以把operator new禁掉,这样new就会报错。
四、设计一个类不能被继承
C++98:
C++11:
五、设计一个只能创建一个对象的类(单例模式)
1.设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
使用设计模式的目的:增强代码的可重用性、可理解性、可靠性,促使代码编写工程化。
2.单例模式
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:
-
饿汉模式
程序启动时就创建一个唯一的实例对象。 (main函数启动前)
但凡是对对象有限制的,比如只能在堆上、栈上创建,第一步,就是将构造函数私有化。
如果不私有化构造函数(因为要创建对象,所以不可能delete构造函数),那么就可以通过构造函数构造多个对象。如下,我们先将构造函数私有化:
class A
{
public:
private:
A()
{}
map<string, string> _dict;
int _n = 0;
};
int main()
{
}
现在我们要创建一个在main()函数调用前就存在的对象,还只能创建一个。我们把问题分解为两个小问题“main函数前创造”和“只能创造一个”,我们先思考前一个问题,什么变量在main()调用前就创造好了你?好像只能是全局变量。
我们试试创建A类型的全局变量_inst。
发现报错,我们没办法直接在类外面定义一个全局变量,那我们可以在类内声明一个变量再在类外定义吗?这不就是类的静态成员变量吗?
类的静态成员只能在类内声明类外定义,我们试试,
class A
{
public:
private:
A()
{}
map<string, string> _dict;
int _n = 0;
static A _inst;
};
A A::_inst;
int main()
{
return 0;
}
我们运行一下程序,发现没有问题。
现在我们只要通过一个类的静态成员函数拿出_inst就行。(注意,一定要是静态的,不然访问非静态的成员函数需要对象或对象的指向,就需要构造对象)
class A
{
public:
static A* GetInstance()
{
return &_inst;
}
private:
A()
{}
map<string, string> _dict;
int _n = 0;
static A _inst;
};
A A::_inst;
可以选择返回指针,也可以返回引用。
因为我们私有了构造函数,同时只创建一个类的静态成员变量,所以不修改当前代码,该类通过构造函数只有这一个对象(后面还要禁拷贝构造)。(同样的,如果定义了两个静态成员变量,该类就可以有两个对象)
那么我们怎么给这个对象添加数据呢?
这本质就是访问其他成员变量(非静态的成员变量),由于A中的成员变量都是私有的,所以我们只要间接的通过成员函数访问私有成员变量即可。
class A
{
public:
static A* GetInstance()
{
return &_inst;
}
void Add(const string& key,const string& value)
{
_dict[key] = value;
}
private:
A()
{}
map<string, string> _dict;
int _n = 0;
static A _inst;
};
A A::_inst;
我们还可以写一个“打印函数”,
class A
{
public:
static A* GetInstance()
{
return &_inst;
}
void Add(const string& key,const string& value)
{
_dict[key] = value;
}
void Print()
{
for (auto& kv : _dict)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
private:
A()
{}
map<string, string> _dict;
int _n = 0;
static A _inst;
};
A A::_inst;
我们开始测试一下这个类。
int main()
{
A::GetInstance()->Add("sort", "排序");
A::GetInstance()->Add("left", "左边");
A::GetInstance()->Add("right", "右边");
A::GetInstance()->Print();
return 0;
}
输出:
我们还需要禁掉拷贝构造,不然可以通过拷贝构造创建对象,如下所示,
这样就创造了两个不同的对象。 所以我们要把拷贝构造禁掉,如果不需要自己给自己赋值,赋值重载也可以禁掉。
饿汉模式的优点:实现简单。
饿汉模式的缺点:1.可能会导致进程启动慢;
2. 如果有两个单例,A类单例和B类单例,分别在不同的文件中,且要区分启动的先后顺序,饿汉模式无法控制启动的先后顺序。
懒汉模式
既然懒汉模式是需要时才用,那我们可以将对象的定义到函数中,调用该函数才会真正创建对象。
为了保证只创建一个对象,我们可以用指针的接受new的对象,这样在函数可以根据指针是否为空,判断是否要创建对象,代码如下,
//懒汉模式:第一次使用时再创建(现吃现用)
class B
{
public:
static B* GetInstance()
{
if (_inst == nullptr)
{
_inst = new B;
}
return _inst;
}
void Add(const string& key, const string& value)
{
_dict[key] = value;
}
void Print()
{
for (auto& kv : _dict)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
private:
B()
{}
B(const B& a) = delete;
B& operator=(const B& a) = delete;
map<string, string> _dict;
int _n = 0;
static B* _inst;
};
B* B::_inst = nullptr;
int main()
{
B::GetInstance()->Add("sort", "排序");
B::GetInstance()->Add("left", "左边");
B::GetInstance()->Add("right", "右边");
B::GetInstance()->Print();
return 0;
}
执行,
既然我们是new出一个对象,该资源怎么释放呢?其实new的懒汉对象一般不需要释放,进程正常结束会释放资源。(这里是指系统在进程结束后释放资源)
那么如果进程不结束,或者要提前释放资源,或者要在main()函数结束后释放资源呢?
我们可以定义一个释放资源的函数,外部可以直接调用释放的,
static void DelInstance()
{
if (_inst)
{
delete _inst;
_inst = nullptr;
}
}
这样我们可以随时调用这个函数释放资源。
同时我们可以定义一个内部类,以及声明一个该类的静态成员变量,然后在类外定义这个变量。内部类的析构函数封装DelInstance(),这样在main()函数运行结束后,编译器会调用内部类静态全局变量的析构函数,也可以达到清理外部类申请资源的作用。
class B
{
public:
static B* GetInstance()
{
if (_inst == nullptr)
{
_inst = new B;
}
return _inst;
}
void Add(const string& key, const string& value)
{
_dict[key] = value;
}
void Print()
{
for (auto& kv : _dict)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
static void DelInstance()
{
if (_inst)
{
delete _inst;
_inst = nullptr;
cout << "delete" << endl;
}
}
private:
B()
{}
B(const B& a) = delete;
B& operator=(const B& a) = delete;
map<string, string> _dict;
int _n = 0;
static B* _inst;
class gc
{
public:
~gc()
{
DelInstance();
}
};
static gc _gc;
};
B* B::_inst = nullptr;
B::gc B::_gc;
int main()
{
B::GetInstance()->Add("sort", "排序");
B::GetInstance()->Add("left", "左边");
B::GetInstance()->Add("right", "右边");
B::GetInstance()->Print();
return 0;
}
运行,
我们没有显示调用 DelInstance(),但还是释放资源了。