文章目录
- 1. 设计一个只能在堆上创建对象的类
- 2. 设计一个只能在栈上创建对象的类
- 3. 设计一个类不能被拷贝
- 4. 设计一个类 不能被继承
- 5. 设计一个类,只能创建一个对象
前言: 在本文中,我们掌握几种常见的特殊类的设计。
1. 设计一个只能在堆上创建对象的类
大部分情况下,我们创建的对象都是在栈上,你只要定义一个对象 那么它就在栈上。 要求对象只能在堆上创建,所以 要对 类的构造函数 做些调整。
思路:
- 将构造函数设置为私有,那么 普通的创建对象方式 就给封死了。
- 提供一个静态成员函数,此函数返回一个 在堆上创建的对象 。
- 拷贝构造 它得销毁掉。
代码实现:
class Heap_class
{
private:
int _a;
Heap_class(int a = 0)
:_a(a)
{}
public:
static Heap_class* Creat_object(int n)
{
return new Heap_class(n);
}
Heap_class(const Heap_class&) = delete;
};
使用:
Heap_class* hc= Heap_class::Creat_object(2);
这就是简单的使用,可以看到这个类中有一个私有成员 _a ,所以设计 出构造这个对象,返回的是一个匿名对象,用传的参数n来初始化_a。
如果 不把拷贝构造销毁掉,会出现这样的情况:
Heap_class* hc= Heap_class::Creat_object(2);
Heap_class hh(*hc);
hh就是在栈上开辟的,所以 还是把拷贝构造封掉。
还有就是 有人可能对 设置一个静态成员函数有问题,为啥要设置为静态呢?
静态成员函数 没有 this指针,它不受对象的管理。简言之,没有对象 它依旧可以调用;如果是普通的成员函数,有this指针,调用普通成员函数,必须先有对象,然而对象 还没创建。这就类似先有鸡后有蛋的问题。
2. 设计一个只能在栈上创建对象的类
上面是只能在堆上,现在要求只能在栈上。和上面的思路如出一辙。
还是得控制 构造函数,如何控制?或者 不需要控制 构造函数,而是去控制 new 、delete
- 设置静态成员函数,返回一个在栈上创建的对象
- 屏蔽new
代码实现:
- 第一个版本:
class stack_class
{
private:
int _a;
stack_class(int a=0)
:_a(a)
{}
public:
static stack_class&& Creat_object(int n)
{
return stack_class(n);
}
};
使用:
stack_class s= stack_class::Creat_object(3);
- 第二个版本:
class stack_class1
{
private:
int _a;
void* operator new(size_t n) = delete;
void operator delete(void* ptr) = delete;
public:
stack_class1(int a=0)
:_a(a)
{}
};
使用: 正常使用就行、
stack_class1 s1(2);
3. 设计一个类不能被拷贝
这就更简单了啊,直接将拷贝构造和赋值重载 delete掉:
class A
{
private:
int _a;
public:
A(int a = 0)
:_a(a)
{}
A(const A&) = delete;
A& operator=(A&) = delete;
};
4. 设计一个类 不能被继承
这个给出两种方法:
- 控制构造函数,将基类的构造函数设置为私有,就会导致继承此基类的子类,无非初始化基类的东西,从而导致构造子类对象失败。这就可以理解为 基类不能被构造,这是C++98的玩法。
- 利用关键字 final 修饰类,这是c++11 才有的。
代码实现:
class B
{
private:
B(){}
};
class s: public B
{
public:
s(){}
};
这里其实就已经编译不通过了,因为 基类的构造函数无法访问。
这里如果不懂为什么,建议去看看我之前的博客,有一篇是专门讲继承的。
class B final
{
public:
B(){}
};
class s: public B
{
public:
s(){}
};
5. 设计一个类,只能创建一个对象
其实这里就涉及到设计模式了,一个类只能创建一个对象,那就是单例模式。单例模式,又有两种实现方式:懒汉,饿汉。
思路:
慢慢来讲这个;首先我提个问题:全局变量 在多个源程序中如何使用?问的有点别扭,我们来看代码:
头文件 声明 还有 一个配套的源程序 定义 ;再来一个源程序有来测验:
hc.h
:
int a;
hc.cpp
:
#include"hc.h"
int a = 1;
test.cpp
:
#include<iostream>
#include"hc.h"
using namespace std;
int main()
{
cout<<a<<endl;
}
像上面那样写,行嘛?看结果:
怎么办?说明一个问题:头文件处对 a变量也是定义,而不是声明,声明一个 变量前 要加 extern
hc.h
:
extern int a;
但是怎么说呢,上面对于一个全局变量 让其他程序共用,用的这种办法是C语言的玩法;到了C++ 我们可以利用面向对象的思想,去设计。
首先,全局变量 让所有程序共用;对应到C++里就是,所有程序,共用一个对象。那么这个类必须 有且只能创建一个对象,也就是单例模式。
- 饿汉模式
hc.h
:
class A
{
private:
int _a;
static A my_A;
A(int a = 0):_a(a)
{}
public:
void add_a()
{
_a++;
}
void del_a()
{
_a--;
}
A(const A&) = delete;
A& operator=(const A) = delete;
public:
static A& use_myA()
{
return my_A;
}
};
A A::my_A; /// 程序入口,就已经创建好了对象
使用:
#include"hc.h"
int main()
{
A::use_myA().add_a();
A::use_myA().add_a();
A::use_myA().add_a();
A::use_myA().del_a();
A::use_myA().del_a();
}
通过调试看看结果:
分析一下饿汉模式:类中有一个静态对象,在程序入口时,已经初始化完成;构造函数依旧给成私有,表明 有且只有一个静态对象供使用;给了两类函数接口,第一个类是static A& use_myA():它是返回了静态对象 ,第二类是void add_a(),void del_a() 操作对象中的成员。
- 懒汉模式
懒汉模式,它不会一上来就创建对象,而是 当用到这个对象时,才会去创建对象,供使用。
class B
{
private:
int _b;
B(int b=0)
:_b(b)
{}
static B* my_B;
public:
static B& use_myB()
{
if (my_B == nullptr)
{
my_B = new B;
}
return *my_B;
}
void add_b()
{
_b++;
}
void del_b()
{
_b--;
}
B(const B&) = delete;
B& operator=(const B) = delete;
};
B* B::my_B = nullptr; // 先给成空指针
懒汉就是 给的不是对象 而是一个对象指针,它为空时,对象不会构建;当使用它时,判断其为 空,那么构造一个对象。
使用也很简单:
B::use_myB().add_b();
B::use_myB().add_b();
B::use_myB().del_b();
但是懒汉模式 还需要 进一步完善,那就是需要考虑线程安全问题,所以需要 使用互斥锁 。
出现线程安全问题的地方:
if (my_B == nullptr)
{
my_B = new B;
}
如果多线程,进来,就有可能导致 构造出多个对象。比如:一个线程判断为空,被打断,另一个线程 进来构建了一个对象,切回到上一个线程,然而 第一个线程已经进入 if语句,所以 它又构造了一个对象。
所以需要在这里加上锁。
还有一点要改善的地方在于,在最后 要回收 静态对象,因为它是在堆上创建的,所以需要内嵌一个回收类。
完整版本:
class B
{
private:
int _b;
B(int b=0)
:_b(b)
{}
static B* my_B;
static mutex _mutex;
public:
static B& use_myB()
{
if (my_B == nullptr)
{
unique_lock<mutex>(_mutex);
if(my_B == nullptr)
my_B = new B;
}
return *my_B;
}
void add_b()
{
_b++;
}
void del_b()
{
_b--;
}
B(const B&) = delete;
B& operator=(const B) = delete;
class reclaim
{
public:
~reclaim()
{
if (my_B)
{
delete my_B;
my_B = nullptr;
}
}
};
static reclaim rm_myb;
};
B* B::my_B = nullptr;
mutex B::_mutex;
B::reclaim rm_myb;
可以看到这就是完整版本的了,有内嵌类型,并且还有一个细节就是 上锁那块。
一般情况下,我们上锁是这样的:
static B& use_myB()
{
unique_lock<mutex>(_mutex);
if (my_B == nullptr)
{
my_B = new B;
}
return *my_B;
}
但是这样上锁,会导致 线程进来都去竞争锁资源。其实只有在 my_B 为空的时候,才需要竞争锁资源去创建一个对象。当对象创建成功,就不需要再竞争锁资源了。因为单例模式嘛,共用一个对象的。
可以优化的 这里:
static B& use_myB()
{
if (my_B == nullptr)
{
unique_lock<mutex>(_mutex);
if(my_B == nullptr)
my_B = new B;
}
return *my_B;
}
但是 必须是 双判断 ,如果 单判断 其实 就相当于没加这个锁:
比如:
static B& use_myB()
{
if (my_B == nullptr)
{
unique_lock<mutex>(_mutex);
my_B = new B;
}
return *my_B;
}
上面这种,就是 单判断 相当于没加锁;给出一种场景,第一个线程判断为空 并竞争上锁资源,创建了一个对象;但在没创建对象成功前,另一个进程 也判断为空成功,并进入了if语句,但是竞争锁资源失败了。第一个线程 创建对象成功后,释放锁资源,第二线程紧接着获取锁资源,然后又创建了一个对象。 嗯 ,问题就出现了。
综上 ,这里需要用到双判断。