目录
🚩什么是特殊类
类模板作为C++面向对象的工具,我们每个程序员可是熟悉到不能再熟悉了。就一般而言,我们在使用类模板的时候,一般诉求都是类的功能越全面越好,方便我们尽可能的去实现多种功能的组合拼装。但是,在某些情况下,类的一些功能需要被舍弃或‘变异’从而达到想要的效果。这就要求我们从最基本的类上经过改装实现特殊类。
🚩有哪些特殊类
特殊类种类繁多,面对不同的应用场景,不同的特殊类应运而生:不能拷贝的类、只能在堆上创建的类、只能在栈上创建的类、不能被继承的类、只能实例化一个对象的类...
这些特殊类看似功能不全,但在某些应用场景下能组装成匪夷所思的更加特殊的类。上面提到的各种类实际上算是最基础的特殊类了。我们就以它们来初步认识一下特殊类吧。
不能拷贝的类
一个类要想不能拷贝,那么它的拷贝构造和赋值构造都不能调用。
class NoCopy
{
public:
NoCopy()
{}
//C++11解决方案-指定删除函数
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
private:
//C98解决方案-设为私有函数
NoCopy(const NoCopy&);
NoCopy& operator=(const NoCopy&);
};
测试:
NoCopy s1;
NoCopy s2(s1); //拷贝构造
NoCopy s3;
s3 = s1; //赋值
结果:
可以看出调用拷贝构造和赋值函数都出现了错误。
只能在堆上创建的类
创建类就涉及到了构造函数,可以考虑把构造函数删除,但是因为要在堆上开辟空间实例化类,因此最好的做法是将构造函数私有化(在堆上创建对象仍然会去调用构造函数),然后在公有成员函数中添加一个静态成员函数,以辅助在不显示调用构造函数的情况下使用new在堆上开辟空间创建对象。这里的拷贝构造函数与赋值函数都要禁止,以防止新的对象是在栈上创建的。
🔺:之所以是静态函数,主要是解决先有鸡还是先有蛋的问题。在没有实例化的对象的情况下仍能够通过类域调用动态开辟函数。
class HeapOnly
{
public:
static HeapOnly* CreatObj()
{
return new HeapOnly; //堆上创建对象
}
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;
private:
HeapOnly()
{}
};
测试:
HeapOnly* ph1 = HeapOnly::CreatObj(); //通过函数动态创建对象
HeapOnly h1; //栈上创建
HeapOnly h2(*ph1); //拷贝构造
HeapOnly* ph2 = ph1; //指针赋值(指针指向的对象在堆区,与设计思路不冲突)
结果:
重点在指针赋值这一块儿,指针在栈上,但指针指向的对象还在堆上。
只能在栈上创建的类
由于只能在栈上创建类,因此使用new在堆上开辟就要禁止了。但是有一点我们得清楚,构造函数还是得放到私有域中去。因为new一个对象就会调用构造函数。这里的赋值和拷贝构造函数不用除去,毕竟拷贝和复制得来的对象还是在栈上。但是,一旦不处理拷贝函数,new就要重载并删除了,因为要避免new调用拷贝构造的情况出现。
class StackOnly
{
public:
static StackOnly CreatObj()
{
return StackOnly();
}
void* operator new(size_t _size) = delete; //指定new删除(使得new StackOnly非法)
private:
StackOnly()
{}
};
测试:
StackOnly* s1 = new StackOnly; //堆上开辟
StackOnly s2=StackOnly::CreatObj();//赋值
结果:
赋值没问题,new在堆区创建对象出错。
不能被继承的类
一个类不能被继承的原因:实例化子类时会调用父类的构造函数。只要将父类的构造函数私有话就成。但是为了不影响父类的正常构造,因此还要写一个静态函数来间接调用构造函数。
class NoInherit
{
public:
static NoInherit CreatObj()
{
return NoInherit(); //间接调用构造函数
}
private:
NoInherit()
{
cout << "NoInherit" << endl;
}
};
class Child : public NoInherit
{
public:
Child()
:NoInherit() //子类继承父类要对父类了进行初始化
{}
};
测试:
Child c1;
结果:
只能实例化一个对象的类(🔺)
这个特殊类相比于前面的都有所不同,由于只能实例化一个对象,因此拷贝构造就得放在私有域中了。但是这仅仅只是基本的保障,如何做到只有一个对象呢?静态数据呀!可以在类里声明一个静态的对象,在类外面进行初始化。类里声明静态对象可以使得该对象能够拥有该类的所有函数和变量的使用权限,类外面初始化自然就可以调用构造函数了。🔺注意这里的拷贝构造和赋值都需要删除,毕竟只能有一个实例化类。
到这里问题基本就解决了,但是会有一个细节需要处理,就是初始化的时候就开辟空间创建对象,还是运行时需要的时候再开辟空间?基于上面的情况分析,这个特殊类又分为饿汉模式和懒汉模式
饿汉模式
形象一点来理解:这个特殊类饥渴难耐,在进程开始的时候就直接先给静态对象初始化并分配空间 。之后就不再创建对象。
//饿汉模式
class Singleton
{
public:
static Singleton* CallObj()
{
return _slt; //返回静态对象
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
void Write()
{
cin >> _str;
}
void Read()
{
cout << _str << endl;
}
private:
Singleton()
{}
static Singleton* _slt; //静态对象声明
string _str;
};
Singleton* Singleton::_slt = new Singleton;//程序开始时就初始化静态对象,并分配空间
测试:
int num = 3;
while (num--)
{
cout << Singleton::CallObj() << endl;//输出每次调用对象的地址
}
结果:
可以通过一个对象观察该对象的地址来验证!
懒汉模式
与饿汉模式刚刚相反,该特殊类很懒,虽然也是在类里声明了一个静态对象,但是在进程开始的时候并不着急分配空间,而是在运行时需要了才会进行空间分配,一点都不着急。
//懒汉模式
class Singleton
{
public:
static Singleton* CallObj()
{
if (_slt == nullptr) //为空的话再决定开辟空间
{
_slt = new Singleton;
}
return _slt;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
void Write()
{
cin >> _str;
}
void Read()
{
cout << _str << endl;
}
private:
Singleton()
{}
static Singleton* _slt;
string _str;
};
Singleton* Singleton::_slt = nullptr; //只初始化,并不开辟空间
测试:
int num = 3;
while (num--)
{
cout << Singleton::CallObj() << endl;//输出每次调用对象的地址
}
结果:
每次地址还是相同,也可以证明只有一个实例化对象。
小结
懒汉模式虽然看着比较佛系,但是如果对象初始化需要较多的资源,可能会出现进程卡在初始化阶段无法进行下面的操作。因此懒汉模式在有些情况下是会延迟加载缓存,只在需要的时候才进行,比较人性化一点🤔。
🚩总结
通过对特殊类的探索不难发现,特殊类主要是对类的基本成员函数进行改动或删除,不同的特殊类之间可能也存在联系:只有一个实例化对象的特殊类其实就涉及到了前面那些特殊类的相关技巧。总的来说,特殊类的使用要看场景,特殊类也不是一成不变的,想要使用的灵活就得真正掌握基本特殊类的特点,进行灵活组合才能得到想要的特殊类。✨