一、特殊类设计方法
1.1设计一个类:不能被拷贝
例如ostream等类,因为缓冲区等缘故进行拷贝消耗极大
做法:
C++98:将拷贝构造和赋值重载设置为private,声明但不实现
C++11:直接把拷贝构造和赋值重载=delete
1.2设计一个类:只能在堆上创建对象
1.2.1在静态区/栈区/堆区上创建对象
静态区:
static Date d1;
栈区:
Date d2;
堆区:
Date* pd3=new Date;
1.2.2创建原理
主要的思路是封锁创建的各种方式,只提供一个接口来进行创建对象
实现起来就是:将构造函数私有化,在接口中实现在堆上创建对象,并将拷贝构造和赋值重载进行delete
注意:接口必须为静态成员函数,否则可能会出现没有对象无法调用成员函数的问题
代码实现:
class OnlyHeap
{
public:
static OnlyHeap* CreatObj()
{
return new OnlyHeap;
}
OnlyHeap(const OnlyHeap& oh) = delete;
OnlyHeap& operator=(const OnlyHeap& oh) = delete;
private:
OnlyHeap() {};
};
int main()
{
OnlyHeap oh1;//无法运行
OnlyHeap* oh2=OnlyHeap::CreatObj();//这是创建对象的唯一方式,且只能够创建堆上的对象
}
1.2.3第二种创建方式
如果说第一种思路是“封前路”,那么思路二就是“堵后路”
把析构私有化,这样以后不能直接在栈上创建对象,因为它要自动调用析构,但此时无法析构
此时直接在堆上new是被允许的(因为new出来的可能是内置类型或者自定义类型,而只有自定义类型需要显式调用析构),
之后我们只需要显式去调用一下DeleteObj来清理资源
class OnlyHeap
{
public:
void DeleteObj()
{
delete this;
}
private:
~OnlyHeap() {};
};
int main()
{
//OnlyHeap oh1;//此时不可以直接创建
OnlyHeap* oh2=new OnlyHeap;
OnlyHeap* oh3=oh2;//创建的oh3也是指向堆上空间
}
1.3设计一个类:只能在栈上创建对象
整体设计思路与堆相似,只需要在CreatObj中return OnlyStack()即可
但要注意:拷贝构造不可以设置为delete,因为OnlyStack()需要调用,因此可能会出现这样的问题:
//假设A为结构体对象
A a1 = A::CreatObj();
A* a2=new A(a1);
对于这个问题可以考虑把operator new置为delete
void* operator new(size_t size) = delete;
1.4设计一个类:不能被继承
C++98:私有化构造
C++11:class A final{};//使用final关键字
1.5设计一个类:只能创建一个对象(即单例模式)
1.5.1设计模式是什么
是一套被反复使用,多数人知晓,经过分类的,代码设计经验的总结
使用的目的是让代码更容易被他人理解,例如此时的单例模式,实现stl容器用到的迭代器模式等等
1.5.2单例模式实现方法一:饿汉模式
所谓单例模式,就是说一个类只能创建一个对象,该模式可保证系统中该类只有一个实例,被所有程序模块共享
所谓饿汉模式,是一种单例模式的实现思路,即“提前创建好对象,避免饿到”
具体实现思路是
①将构造函数私有化,提供一个create接口,需要用到这一对象时可以通过接口直接获取
②设置一个静态成员变量为本身;再去类外声明一次,保证在编译时创建
③拷贝构造和赋值重载有破坏单例模式的风险,需要用delete封一下
代码示例:
class InfoMgr
{
public:
static InfoMgr& GetInstance()
{
return _ins;
}
void Print()
{
cout << _ip << endl;
cout << _port << endl;
cout << _buffSize << endl;
}
private:
InfoMgr(const InfoMgr&) = delete;
InfoMgr& operator=(const InfoMgr&) = delete;
InfoMgr()
{}
private:
string _ip = "127.0.0.1";
int _port = 80;
size_t _buffSize = 100;
static InfoMgr _ins;
};
InfoMgr InfoMgr::_ins;
int main()
{
InfoMgr info1=GetInstance();
InfoMgr info2=GetInstance();
return 0;
}
饿汉模式有一些缺陷:
①如果有多个饿汉模式的单例,初始化的内容会比较多,启动缓慢
②A和B两个饿汉,对象初始化的时候有依赖关系,要求先A后B,饿汉无法保证
1.5.3单例模式实现方法二:懒汉模式
所谓懒汉模式,是一种单例模式的实现思路,即“在第一次调用的时候才创建对象,绝不勤快”
实现思路是将自身的静态成员变量设置为指针,在类外初始化给nullptr
在GetInstance()函数中判断一些是否为nullptr,如果是再申请空间
但是这样实现是有线程安全的风险的,为此C++11特地支持了局部静态成员变量的申请,以此应对这一风险。
示例代码:
class InfoMgr
{
public:
static InfoMgr& GetInstance()
{
static InfoMgr ins;
return ins;
}
void Print()
{
cout << _ip << endl;
cout << _port << endl;
cout << _buffSize << endl;
}
private:
InfoMgr(const InfoMgr&) = delete;
InfoMgr& operator=(const InfoMgr&) = delete;
InfoMgr()
{}
private:
string _ip = "127.0.0.1";
int _port = 80;
size_t _buffSize = 1;
};
二、C++的类型转换
2.1内置类型之间相助转换
隐式类型转换:整形家族/整形与浮点数之间
显式(强制)类型转换:如指针和整形,相互间有一定关联关系的类型
2.2内置类型与自定义类型之间转换
2.2.1内置类型->自定义类型
通过构造函数支持,如单参数构造函数隐式类型转换,多参数构造函数隐式类型转换
用起来可以是A a1 = 1;这样即可构造
2.2.21补:explicit修饰构造函数
正常情况下只要支持了单参数构造,就可以使用单参数构造函数隐式类型转换
但如果使用explicit修饰了构造函数
如 explicit A(int a)
就不支持隐式类型转换了,但依旧可以通过强势类型转换实现
2.2.2自定义类型->内置类型
通过运算符重载支持,(原本C语言强制类型转换用的是(),但C++中()的重载逻辑已经给了仿函数,所以推出了一种特殊的重载方式)
如A类型->int类型
class A
{
//...
operator int()
{
return _a1;
}
//...
};
//在main函数调用
int main()
{
A aa1(5);
int a=aa1;//就是int a=aa1.operator int()
return 0;
}
2.3自定义类型之间相互转换
通过构造函数重载来支持,如在结构体B中实现一个A的重载
B(const A& aa)
:_b(aa._a)
{}
2.4特殊情况:const迭代器接收普通迭代器对象完成遍历
把普通对象传给const对象时,发生权限的缩小问题以及权限的放大都是只针对于const修饰的指针和引用类型对象,迭代器类型并不属于
因为
所以它是两个单独的类,属于类型转换
因此直接把普通迭代器给const迭代器是不可行的
需要考虑用构造函数来支持自定义->自定义
可以直接参考库中的做法:
①传入普通迭代器类型时,属于拷贝构造
②传入const迭代器类型,属于普通构造
二者共同点是都创建了const迭代器对象
2.5C++中对于类型转换的规范
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符
static_cast,reinterpret_cast,const_cast,dynamic_cast
2.5.1static_cast
对应隐式类型转换,数据本身意义不变
如将浮点数丢掉一些精度转换为整形
double d=3.14;
int a=static_cast<int>(d);
2.5.2reinterpret_cast
对应强制类型转换,数据意义改变
如将int*类型转换为int类型
int* a1=new int;
int a2=reinterpret_cast<int>(a1);
2.5.3const_cast
对应强制类型转换中 “有风险地去掉const属性”
如
const int a=10;
int* pa=const_cast<int*>(&a);
*pa=100;
注意:此时如果打印会出现问题
为什么a没有被修改呢?
原因是编译器对const修饰地内容进行了“优化”:
C++中const修饰的部分实际上是常变量, 所以去掉const属性后他们还可以被修改,但这不同于C语言中的常量,为此编译器将常变量进行了部分优化,使得在进行访问操作时,const地内容只遵循第一次设置的内容
可以使用”volatile“关键字来取消优化
2.5.4dynamic_cast
2.5.4.1情景设置
这是一个应用于继承体系下的类型转换,用来避免父类对象接收子类对象后造成的安全问题
假设此时有这样一个基类和派生类:
①派生类中有自己的成员变量
②基类中有一个虚函数
struct AA
{
public:
virtual void func(){}
int _a = 0;
};
struct BB:public AA
{
int _b = 1;
};
他们在一个函数中被调用,
①此函数传入的是基类对象
②函数中需要把基类对象进行类型转换为派生类对象
③函数中对基类和派生类对象进行访问
void func1(AA* aa)
{
BB* bb = (BB*)aa;
cout << bb->_a << endl;
cout << bb->_b << endl;
}
那么我们在调用函数的时候,都可能出现哪些情况呢?
①传入的是基类对象的指针
②传入的是派生类对象的指针
AA a;
BB b;
func1(&a);//传入基类对象的指针
func1(&b);//传入派生类类对象的指针
此时打印会发现
2.5.4.2问题提出
这很明显出现了问题
我们传入的是基类指针,却在访问派生类对象的专属成员变量
这是什么缘故呢?
我们已知
向上转型:子类对象/引用->父类对象/引用不需要转换,赋值是兼容的
向下转型:父类对象/引用->子类对象/引用有风险
在func1()中,
B* pb1=(B*)pa;就是父类转子类
对它来说,用static_cast和reinterpret_cast都是不安全的
传入父类指针,但是被转换成子类并访问子类专属元素_b是不会被禁止的
访问会读取出随机数,只有写会报错
2.5.4.3问题解决
所以func1中的强制类型转换风险极大,可以改为更好的 dynamic_cast
此时如果我们选择使用dynamic_cast,会自动检测,能成功则转换 (原始指向子类对象的时候可以成功),不能成功则返回NULL (原始指向父类对象的时候不能成功)
BB* bb = dynamic_cast<BB*>(aa);
2.5.4补:为什么基类要有虚函数
因为使用dynamic_cast需要多态(父类中需要有虚函数)
dynamic_cast的本质是去虚表中通过标识来确定指针原始指向的
2.6RTTI运行机制
是Run-timeType identification 即运行时类型识别,可以称其为一种编程语言特性
它在C++中的体现如typeid运算符,dynamic_cast运算符,decltype类型识别