目录
一、特殊类的设计
1.不能被拷贝的类
2.只能在堆区构建对象的类
3.只能在栈区构建对象的类
4.不能被继承的类
二、单例模式
1.饿汉模式
2.懒汉模式
3.线程安全
4.单例的释放
三、C++类型转换
1.C语言的类型转换
2.static_cast
3.reinterpret_cast
4.const_cast
5.dynamic_cast
6.总结
一、特殊类的设计
特殊类就是普通类的基础上加上一些限制条件,虽然用处不大,但是有相关需要的时候这些还是能提供思路的。
1.不能被拷贝的类
由于类的拷贝只会发生在拷贝构造函数和赋值运算符重载中,所以不允许调用拷贝构造函数和赋值运算符重载函数即可实现。
C++98中可以使用对拷贝构造函数和赋值运算符重载函数只声明不定义且声明私有的方式实现。编译器都会自动生成默认成员函数,两函数可自动生成,但声明在私有里,定义也就只能在私有里,所以这两个函数在类外面还是调不了。
class copy_ban
{
public:
copy_ban()
{}
private:
copy_ban(const copy_ban& cb);
copy_ban& operator=(const copy_ban& cb);
};
测试拷贝报错,而构造正常
C++11中增加了delete的用法,我们可以在两函数声明后加上=delete让编译器不再生成两函数,从根本上杜绝了拷贝的发生。
class copy_ban
{
public:
copy_ban()
{}
private:
copy_ban(const copy_ban& cb) = delete;
copy_ban& operator=(const copy_ban& cb) = delete;
};
2.只能在堆区构建对象的类
我们正常定义的局部变量都存储在栈区,要想让变量只占用堆区,我们需要让该类的对象只能通过new创建并使用构造函数初始化。
因为构造函数创建的对象在栈上,构造函数应当私有,而且不能参与资源申请。
既然构造函数不能用于构造对象,那么就需要在类中实现一个函数用于构建对象,而我们知道类内的普通成员函数第一个参数都是this指针,而如果一个类都没有对象,又何来this指针?
而静态成员函数可以直接在类域内调取,所以这个公有的创建对象的成员函数应为静态,而且返回的是new对象。
拷贝构造函数也会将对象构造在栈区,故也需要禁止。
class heap_only
{
public:
static heap_only* create_ho()
{
return new heap_only;
}
private:
heap_only()
{}
};
测试代码正常报错
3.只能在栈区构建对象的类
对于C++的内存的管理,只要你屏蔽了new和delete也就不能从堆上申请空间了。
我们使用的new和delete在底层也是两个函数void* operator new(size_t size)和void operator delete(void* p),只要在它们声明后面加上delete就可以了。
class stack_only
{
public:
stack_only()
{}
private:
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
};
你要非要用malloc这种C接口也拦不住你,不过这些也都是可以禁的。
4.不能被继承的类
一个派生类的构造是需要调用其基类的构造函数的,所以将一个类的构造函数私有也就不能被继承了。
class no_inherit
{
private:
no_inherit()
{}
};
class non : public no_inherit
{};
测试报错
不过,这样一个不能构建对象的类也没有价值,所以C++11引入了关键字final,final修饰的类叫最终类,不能被继承。
class no_inherit final
{
public:
no_inherit()
{}
private:
};
class non : public no_inherit
{};
从语法上拒绝继承也不影响基类
二、单例模式
设计模式是编程中被反复使用,大多数人也都知道,最终经过分类整理的代码设计经验总结,也可以认为是代码的设计模板,按照模板编写的程序更加高效稳定。而单例模式就是其中一种设计模式,下面讲到的单例其实也可以归类到特殊类里。
单例模式的类需要保证系统中该类只有一个实例并提供一个用于访问它的全局访问点,而且该实例需要被所有程序模块共享。
单例模式有以下特征:
- 因为全局只允许存在单个对象,所以单例对象常放在静态区或者堆区(只创建一次)。
- 为了防止在其他位置创建该对象,构造函数必须私有。
- 为了防止拷贝,需要禁用拷贝构造和赋值运算符重载函数。
单例模式有两种实现方式:饿汉模式和懒汉模式。
1.饿汉模式
在单例类内直接定义一个静态的类对象,静态成员变量在类域外进行初始化。在没有定义变量时静态成员就已经初始化完毕,也就是main函数开始执行语句前单例就已经建立完成。静态区也不可能再建立第二个变量,保证了单例的唯一性。
获取单例指针可以用静态函数get_objection()返回单例对象的指针,通过指针可以实现内部成员函数的调用。
class one_obj
{
public:
static one_obj* get_objection()//获取单例对象的接口
{
return &_obj;
}
private:
one_obj()
{}
one_obj(const one_obj& o) = delete;//不允许拷贝构造
one_obj& operator=(const one_obj& o) = delete;//不允许赋值
static one_obj _obj;//单例对象,设为静态保证独一份
};
one_obj one_obj::_obj;//根据one_obj初始化
不管以后会不会使用这个单例对象,只要程序一启动,程序就会先创建一个唯一的实例对象然后再执行main中的代码。就很像一个饿汉看到吃的就直接扑上去吃,这个上去就吃的动作就相当于创建单例对象。
饿汉模式也有缺点:
- 可能会减慢程序的启动。比如实例对象很复杂,而饿汉模式又必须优先创建单例对象,启动就会花费很多时间。
- 实例顺序不确定。如果有多个单例对象且各对象之间存在互相依赖关系,由于单例的实例化是再main函数之前完成的,所以对象的实例顺序是由编译器决定的。如果单例初始化的顺序不合适,就会发生错误。
2.懒汉模式
懒汉模式的构造函数也是私有,拷贝构造和赋值运算符重载函数也禁止调用,但是把静态成员变量改成了静态单例对象的指针。在类外实例化静态指针变量的时候,将其初始化为空。
我们将单例对象的构造放在了获取单例指针的静态函数get_objection()中,在第一次调用这个函数时,内部就会构造出单例对象并返回指针,由于是单例对象,以后也不需要再构造直接返回指针即可。
class one_obj
{
public:
static one_obj* get_objection()
{
if (_ptr == nullptr)
{
_ptr = new one_obj;
}
return _ptr;
}
private:
one_obj()
{}
one_obj(const one_obj& o) = delete;
one_obj& operator=(const one_obj& o) = delete;
static one_obj* _ptr;
};
one_obj* one_obj::_ptr = nullptr;
正因为单例只有在第一次被使用到时才被建立,所以懒汉模式又叫延时加载模式,就像一个懒汉一样,什么事都拖到截止日才干。
如果单例构造耗时或者占用资源多就有可能导致程序启动时非常的缓慢,而这种情况下懒汉模式就更合适。
懒汉模式的优点:
- 第一次使用单例对象时才创建对象,进程启动过程无负载。
- 多个互相依赖的单例可以通过代码控制构造顺序。
3.线程安全
C++11解决了饿汉模式的线程安全问题,因为单例对象是在main函数之前就实例化的,而多线程都是在main函数里面启动的。
但是懒汉模式是存在线程安全问题的,当多个线程第一次使用单例对象时,get_objection()获取对象因为调度问题就可能会出现误判,导致构造多个单例对象。
所以我们就需要对get_objection()进行双检查加锁。
第一版
很多人都会写成这个错误的版本,我们加锁是为了不允许两个线程同时进入第二层的判断语句。这样的加锁只保证了多个线程不会同时new出多个单例对象。所以加锁必须在第二层判断语句的外部。
第二版
这一版就是正确的了,有些人可能不理解为什么要判断两次。首先第一次调用get_objection()时,单例没有构造_ptr为空,进入第二层,加锁保证不会有第二个线程进入判断。第二层也通过了就直接构造单例对象即可,最后返回_ptr。
而如果有线程同时进入第一层,此时_ptr就不为空了,当然也不会构造单例。
如果没有最外层的判断,那么每一个线程进来一次都需要加锁解锁一次,这样会增大无故的开销。所以这层判断只要不符就证明单例构建好了,直接返回指针就好了,不要再加锁了。
4.单例的释放
new出的单例对象一般情况我们都不释放,因为全局只有一个单例对象,而且在程序运行时也会一直被使用。当程序结束的时,操作系统会回收该进程的所有资源,包括堆区上的资源,所以也没必要释放。
如果非得主动释放,基本也是在释放的同时将一些信息保存到磁盘,比如错误日志什么的。而单例的回收可能较为复杂,内部类又是外部类的友元,所以我们可以使用内部类析构。
最终代码如下:
#include<mutex>
class one_obj
{
public:
static one_obj* get_objection()
{
if (_ptr == nullptr)
{
_mtx.lock();
if (_ptr == nullptr)
{
_ptr = new one_obj;
}
_mtx.unlock();
}
return _ptr;
}
class recycle
{
public:
~recycle()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
//保存数据到磁盘,这里是读写磁盘的代码
}
}
};
private:
one_obj()
{}
one_obj(const one_obj& o) = delete;
one_obj& operator=(const one_obj& o) = delete;
static one_obj* _ptr;
static std::mutex _mtx;
static recycle _rec;
};
one_obj* one_obj::_ptr = nullptr;
std::mutex one_obj::_mtx;
one_obj::recycle one_obj::_rec;
两种模式各有其优点和缺点:懒汉模式中需要双检查加锁,考虑线程安全,相比于饿汉模式复杂,所以饿汉模式的优点就是简单明了,而懒汉模式的缺点就是复杂细节多。两种模式的使用要要根据具体应用场景决定。
懒汉模式还有一种实现方式,这次是在get_objection()被调用时创建静态对象,静态对象不会被第二次创建,保证了单例的唯一性。
因为禁止了拷贝构造,所以调用成员函数时不能使用one_obj obj = one_obj::get_objection()的方式,而是直接使用get_objection().print()才能正常使用。
#include<iostream>
class one_obj
{
public:
static one_obj& get_objection()
{
static one_obj o;
return o;
}
void print()
{
std::cout << "one_obj" << std::endl;
}
private:
one_obj()
{}
one_obj(const one_obj& s) = delete;//禁止拷贝
one_obj& operator=(const one_obj& s) = delete;//禁止赋值
};
int main()
{
one_obj::get_objection().print();
return 0;
}
三、C++类型转换
1.C语言的类型转换
C语言中,如果赋值运算符(=)两边的变量类型不同,函数形参实参类型不匹配,还有返回值类型和接收变量类型不一致,都需要进行类型转换。
C语言有两种类型转换:隐式类型转换和显式类型转换。
隐式类型转换是编译器在编译阶段自动完成的,类型能转换就转换,不能就编译失败。
显式类型转换是用户自己处理的转换,主要是对两种没有任何关系的类型进行转换,比如将指针类型转换成整型等。
#include<stdio.h>
int main()
{
int a = 1;
double b = a;//隐式类型转换
printf("%d %f\n", a, b);
int* p = &a;
int num = (int)p;//显式类型转换
printf("0x%p %d\n", p, num);
return 0;
}
C语言的类型转换也存在一定缺陷:
隐式类型转换有时会发生数据精度丢失,比如整形提升等。
显式类型转换允许很多底层结构相似的类型相互转化,所有情况混合在一起,代码不够清晰。
为了解决这些不足,C++也建立了自己的类型转换结构,C++作为C语言的超集,C语言的转换依旧可以使用。
2.static_cast
C语言的隐式类型转换在C++中统一使用static_cast转换,它不能用于两个不相关的类型进行转换。
#include<iostream>
int main()
{
//C语言
int a = 1;
double b = a;
printf("%d %f\n", a, b);
//C++
int c = 1;
double d = static_cast<double>(c);//隐式类型转换
printf("%d %f\n", c, d);
return 0;
}
3.reinterpret_cast
C语言的显式类型转换在C++中统一使用reinterpret_cast,如果使用static_cast会报错,它专用于将一种类型转换为另一种无关的类型。
#include<iostream>
int main()
{
//C语言
int a = 1;
int* p1 = &a;
int num1 = (int)p1;
printf("0x%p %d\n", p1, num1);
//C++
int b = 1;
int* p2 = &b;
int num2 = reinterpret_cast<int>(p2);
printf("0x%p %d\n", p2, num2);
return 0;
}
4.const_cast
常变量本身的常属性是在其定义时就规定好的,以后也都不可改变。而C++中使用const_cast可以去除常变量的常属性,此时变量就可以修改了。
要注意,const_cast只能将常变量的指针作为参数并返回一个普通指针,变量可以通过普通指针进行修改。
#include<iostream>
int main()
{
const int a = 1;
const int* p1 = &a;//指针为const int*,不能改变a值
int* p2 = const_cast<int*>(p1);//p1转为int*并用p2接收
*p2 = 3;
printf("%d", a);
return 0;
}
5.dynamic_cast
dynamic_cast可以将父类对象的指针或者引用转为子类对象的指针或引用。
对于继承的指针与引用转化分为向上和向下转换。
向上转换是子类对象的指针或引用转为父类对象的指针或引用。C++语法直接支持,不会发生类型转换。
向下转换是父类对象的指针或引用转为子类对象的指针或引用。语法不支持,但dynamic_cast可以支持。
dynamic_cast的转化有以下特性:
- dynamic_cast只能用于父类含有虚函数的类。
- dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回nullptr。
- dynamic_cast是父子指针的安全转换方式,C语言的转换方式可能出现越界访问。
#include<iostream>
//父类
class A
{
public:
virtual void f()
{}
int _a = 1;
};
//子类
class B : public A
{
public:
int _b = 2;
};
int main()
{
A a;
B b;
A* p1 = &a;//父类指针指向父类对象
B* p2 = &b;//子类指针指向子类对象
A* p3 = &b;//父类指针指向子类对象,不需要类型转换
B* p4 = dynamic_cast<B*>(&a);
//使用dynamic_cast转化指针,指向父类对象的父类指针转为子类指针
B* p5 = dynamic_cast<B*>(p3);
//使用dynamic_cast转化指针,指向子类对象的父类指针转为子类指针
std::cout << p1 << " " << p2 << " " << p3 << " " << p4 << " " << p5 << std::endl;
return 0;
}
测试结果:
对于上述常见指针,在p3、p4、p5之中我们发现只有p4不能正常转化。
这是因为指向父类对象的父类指针如果转为了子类指针,它所指向的对象没有子类的部分,这就可能会造成越界访问,当然不会成功。
而将指向子类对象的父类指针转为子类指针可使该指针维护的内容覆盖整个子类数据,因为父类指针不能赋值给子类指针,所以就需要dynamic_cast转化。
C++中的类型转换,尤其是前两种static_cast和reinterpret_cast是建议用法,可以采用也可以不采用。const_cast是一种新用法,但是存在风险,dynamic_cast是一种安全的类型转换。
6.总结
C++推出了一套自己的变量类型转换方式,除了dynamic_cast是在多态转换中必须使用外,其他三种方式也仅是建议使用,可以增加代码的规范性。