目录
- 特殊类的设计
- 不能被拷贝的类
- 实现一个类,只能在堆上实例化的对象
- 实现一个类,只能在栈上实例化的对象
- 不能被继承的类
- 单例模式
- 饿汉模式
- 懒汉模式
- 饿汉模式与懒汉模式的对比
- 饿汉优缺点
- 懒汉优缺点
- 懒汉模式简化版本(C++11)
- 单例释放问题
- C++强制类型转换
- static_cast 静态转换
- reinterpret_cast 不同类型转换
- const_cast 去除变量的const属性
- dynamic_cast 动态转换
特殊类的设计
不能被拷贝的类
一个类能不能实现拷贝功能,要看两个默认成员函数:拷贝构造 与 赋值重载
要实现一个不能被拷贝的类,就要防止拷贝构造 与 赋值重载这两个函数的生成。
在类中,如果我们不去手动实现默认成员函数的话,编译器会自动生成默认的成员函数(像 构造、析构 拷贝构造 与 赋值重载等)
在C++11之前,为了避免编译器自动生成 拷贝构造 与 赋值重载,可以将这两个函数直接进行声明,不去实现。但是直接声明的方式,防不住类外的人进行定义实现。
对此,可以直接将 拷贝构造 与 赋值重载 声明设置为私有。
class CopyBan
{
public:
CopyBan(){}
private:
//将拷贝构造与赋值重载设置为私有
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
};
int main()
{
CopyBan cb1;
CopyBan cb2(cb1); //error
return 0;
}
C++11标准出来后,拓展了 delete 关键字 的使用。如果要让编译器不生成默认的成员函数,直接在这个成员函数后加上 delete 关键字即可。对此,上面的代码可以改写为:
class CopyBan
{
public:
CopyBan(){}
//让编译器删除掉默认成员函数
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
};
int main()
{
CopyBan cb1;
CopyBan cb2(cb1); //error
return 0;
}
这样就实现了一个不能被拷贝的类了。
实现一个类,只能在堆上实例化的对象
实现这个类有两种方法,先来介绍第一种:将析构函数设置为私有
class HeapOnly
{
public:
HeapOnly(int x = 0)
:_x(x)
{}
private:
~HeapOnly() //设置为私有
{
cout << "~HeapOnly()" << endl;
}
int _x;
};
int main()
{
HeapOnly hoy(1); //error 会调用析构
return 0;
}
hoy 对象在程序运行结束后会调用析构函数,因为析构函数是私有的,调不动。对此,只能在堆上实例化对象:
int main()
{
HeapOnly* hoy = new HeapOnly(1);
return 0;
}
但是,如何去释放资源呢?
可以这样,在类中实现一个成员函数去调用析构函数:
class HeapOnly
{
public:
HeapOnly(int x = 0)
:_x(x)
{}
void DestroyHeap()
{
delete this; //调用析构
}
private:
~HeapOnly()
{
cout << "~HeapOnly()" << endl;
}
int _x;
};
int main()
{
HeapOnly* hoy = new HeapOnly(1);
hoy->DestroyHeap(); //利用成员函数去调用析构函数
return 0;
}
下面来介绍第二种方法:将构造函数设置为私有
class HeapOnly
{
public:
~HeapOnly()
{
cout << "~HeapOnly()" << endl;
}
private:
HeapOnly(int x = 0) //将构造函数设置为私有
:_x(x)
{}
int _x;
};
注意:这个方法会造成,在用 new 的时候也实例化不出对象:
int main()
{
HeapOnly hoy1(1); //error
static HeapOnly hoy2(1); //error
HeapOnly* hoy3 = new HeapOnly(1); //error
return 0;
}
解决办法:通过成员函数来调用构造函数,只不过要将这个成员函数要设置为静态的
利用静态成员函数,去调用构造函数,返回这个对象的指针即可:
class HeapOnly
{
public:
static HeapOnly* CreateObje(int x) //利用静态成员函数去调用构造
{
HeapOnly* hoy = new HeapOnly(1);
return hoy;
}
~HeapOnly()
{
cout << "~HeapOnly()" << endl;
}
private:
HeapOnly(int x = 0)
:_x(x)
{}
int _x;
};
int main()
{
HeapOnly* hoy = HeapOnly::CreateObje(1); //调用静态成员函数
delete hoy; //释放资源
return 0;
}
上面这个方法还需要考虑一个拷贝问题:
int main()
{
HeapOnly* hoy = HeapOnly::CreateObje(1);
HeapOnly hoy2(*hoy); //拷贝,在栈上开辟
delete hoy;
return 0;
}
构造被封死,但是拷贝构造没有。上面代码没有实现拷贝构造,但是编译器会自动生成一个。拷贝实例化出的对象是在栈上开辟,不符合案例的要求。对此,可以直接将拷贝与赋值直接禁用:
class HeapOnly
{
public:
static HeapOnly* CreateObje(int x)
{
HeapOnly* hoy = new HeapOnly(1);
return hoy;
}
~HeapOnly()
{
cout << "~HeapOnly()" << endl;
}
private:
HeapOnly(int x = 0)
:_x(x)
{}
//禁用拷贝与赋值
HeapOnly(const HeapOnly& hop) = delete;
HeapOnly& operator=(const HeapOnly& hop) = delete;
int _x;
};
实现一个类,只能在栈上实例化的对象
使用 new 关键字会调用构造函数,为了防止在堆上开辟对象。可以直接将拷贝构造设置为私有,通过静态成员的方式去调用构造函数:
class StackOnly
{
public:
//利用静态成员函数进行实例化对象
static StackOnly CreateObje(int x)
{
return StackOnly(x); //返回对象
}
//拷贝
StackOnly(const StackOnly& soy)
:_x(soy._x)
{}
~StackOnly()
{
cout << "~StackOnly()" << endl;
}
private:
StackOnly(int x) //构造函数设置为私有
:_x(x)
{}
int _x;
};
int main()
{
//StackOnly* soy = new StackOnly(1); //error new会调用构造
StackOnly soy = StackOnly::CreateObje(1); //调用构造+拷贝构造
return 0;
}
上面这样的方式防不住创建静态的对象:
int main()
{
StackOnly soy = StackOnly::CreateObje(1); //调用构造+拷贝构造
static StackOnly soy1 = soy; //会利用拷贝构造生成一个静态的对象
return 0;
}
有小伙伴就会说,将拷贝构造禁用,就可以避免静态对象的生成了。
如果将拷贝构造禁用了,利用静态成员函数去实例化的方法会失效,因为用到的传值返回,需要用到拷贝构造。
传值返回是一个将亡值,那么可以生成一个移动构造不就解决了吗?
下面来实现一下:
class StackOnly
{
public:
//利用静态成员函数进行实例化对象
static StackOnly CreateObje(int x)
{
return StackOnly(x);
}
StackOnly(StackOnly&& soy) // 移动构造
:_x(soy._x)
{}
~StackOnly()
{
cout << "~StackOnly()" << endl;
}
private:
StackOnly(const StackOnly& soy) = delete; //禁用拷贝
StackOnly(int x)
:_x(x)
{}
int _x;
};
int main()
{
StackOnly soy = StackOnly::CreateObje(1); //移动构造
//static StackOnly soy1 = soy; //error
return 0;
}
但是,使用移动构造真的解决问题了吗?传值返回是一个将亡值,会去调用移动构造。但是,防不住有些人这样操作:
int main()
{
StackOnly soy = StackOnly::CreateObje(1); //拷贝构造
static StackOnly soy1 = move(soy); //将soy设置为将亡值,调用移动构造
return 0;
}
使用静态成员函数去调用构造函数,通过传值返回对象的方式,很难去防止静态对象生成。很难禁掉,本质就是前者需要用到拷贝构造
不能被继承的类
在C++11之前,可以将类的构造函数设置为私有。子类对象如果继承了这个类的话,在实例化阶段就调不动父类的构造函数,从而达到无法继承的效果。
C++11之后,推出了 final
关键字。被 final
关键字修饰过类,不能被继承:
class NonInherit final //final关键字修饰的类不能被继承
{
//...
}
单例模式
- 单例模式:一个类只能创建一个唯一的对象
该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享
实现单例模式的方式有两种:饿汉模式、懒汉模式
饿汉模式
在程序运行起来就实例化出这个对象,且为唯一的一份
记住单例模式要的是创建一个唯一的对象
举个示例,实现一个简单的饿汉模式:
要限制这个类实例化对象,将构造函数设置为私有、拷贝和赋值禁用
#include <iostream>
#include <string>
#include <vector>
class Singleton
{
public:
~Singleton() //析构
{
std::cout << "~Singleton()" << std::endl;
}
private:
Singleton(){} //设置为私有,防止类外实例化对象
Singleton(const Singleton& stn) = delete;
Singleton& operator=(const Singleton& stn) = delete;
private:
vector<string> _v;
int _n = 0;
};
下面来考虑一下,如何实例化出唯一的一份对象。
可以定义一个
Singleton* _ins
静态成员指针,当然也可以是Singleton _ins
对象(静态成员在整个类都是唯一的)。
在这里定义一个 Singleton* _ins
的静态成员指针,在类中进行声明,在类外进行实例化。当然还要实现一个静态成员函数,用于获取这个静态成员指针。
为了方便演示,下面还增添一些功能代码:
#include <iostream>
#include <string>
#include <vector>
class Singleton
{
public:
~Singleton() //析构
{
std::cout << "~Singleton()" << std::endl;
}
static Singleton* GetInstance() //用于获取对象指针
{
return _ins; //返回静态成员的指针,方便在类外部使用
}
void AddString(const string& str){ _v.push_back(str);}
void Print()
{
for (auto& e : _v)
cout << e << endl;
}
private:
Singleton(){} //设置为私有,防止类外实例化对象
Singleton(const Singleton& stn) = delete;
Singleton& operator=(const Singleton& stn) = delete;
private:
vector<string> _v;
int _n = 0;
static Singleton* _ins; //声明静态成员指针
};
Singleton* Singleton::_ins = new Singleton; //初始化
int main()
{
Singleton* sln = Singleton::GetInstance(); //获取对象的指针,唯一的
sln->AddString("苹果");
sln->AddString("香蕉");
sln->AddString("梨");
sln->AddString("西瓜");
sln->Print();
return 0;
}
一开始就通过静态成员指针或者是对象直接在类外初始化的方式被饿汉模式,也就是在 main 函数之前进行初始化。
与 饿汉模式 相对应的就是 懒汉模式
懒汉模式
懒汉模式与饿汉模式不同,并不是一上来就进行初始化。而是在第一次访问实例的时候进行初始化
对上述代码进行修改,变成懒汉模式。下面只展示修改的代码,方便对比:
class Singleton
{
public:
~Singleton()
{
cout << "~Singleton()" << endl;
}
//懒汉处理
static Singleton* GetInstance()
{
if (_ins == nullptr)
{
_ins = new Singleton; //懒汉模式的初始化
}
return _ins;
}
//... 其他成员函数
private:
Singleton() {} //设置为私有,防止类外实例化对象
Singleton(const Singleton& stn) = delete;
Singleton& operator=(const Singleton& stn) = delete;
vector<string> _v;
static Singleton* _ins; //定义静态Singleton指针
};
//懒汉模式
Singleton* Singleton::_ins = nullptr;
懒汉模式并不是一上来就进行初始化,而是将静态成员对象的指针的初始化放到静态成员函数内部进行。
下面来测试一下懒汉模式:
int main()
{
Singleton* sln = Singleton::GetInstance();
sln->AddString("苹果");
sln->AddString("香蕉");
sln->AddString("梨");
sln->AddString("西瓜");
sln->Print();
return 0;
}
有老铁就会说,这个 懒汉模式 和 饿汉模式 也没有多少变化啊。只是初始化方式不同而已。
但真的只是初始化有所不同吗?
饿汉模式与懒汉模式的对比
饿汉优缺点
先来分析饿汉模式缺点:
- 当一个单例对象很大
- 当这个单例对象不需要使用时,会占用资源(main函数之前就要进行资源的申请)
- 会影响程序的启动(程序在还没有启动前就要初始化一段时间)
- 当一个程序中有两个单例对象都是使用的饿汉模式,并且相互之间存在依赖关系。要求单例1对象先创建,单例2对象后创建。饿汉模式无法控制创建的顺序
饿汉模式的优点:创建单例对象很简单(相较于懒汉模式)
懒汉优缺点
优点:懒汉模式完美解决了饿汉模式的缺点
缺点:懒汉模式会造成线程安全问题
回过头来看看懒汉模式下的初始化那段代码,同样的为了方便查阅,只展示局部代码:
//懒汉处理
static Singleton* GetInstance()
{
if (_ins == nullptr)
{
_ins = new Singleton; //懒汉模式的初始化
}
return _ins;
}
有没有这样的一种情况:在多线程下,有两个甚至多个线程同时进行 new 对这个单例进行初始化,造成 _ins
指针被多次赋值
线程执行总得有个先后顺序,假设线程一先执行了初始化的工作,正在调用其他函数执行其他的功能。此时,线程二来了,直接进行 new 操作申请了一块新的资源空间,然后赋值给 _ins。线程一前面执行的函数(像是读写操作),就会被覆盖了做无用功。
相对比懒汉模式,饿汉模式就没有线程安全问题,因为饿汉模式在 main 程序执行前直接就进行了初始化。
对此,实现懒汉模式的单例需要引入互斥锁来解决线程安全问题。
通过双检查加锁来处理线程安全问题:只需要保证第一次加锁的情况
class Singleton
{
public:
~Singleton()
{
cout << "~Singleton()" << endl;
}
static Singleton* GetInstance()
{
//懒汉线程安全问题的处理
//双检查加锁:解决线程安全问题
if (_ins == nullptr)
{
//上锁
_mtx.lock();
if (_ins == nullptr)
{
_ins = new Singleton; //保证一次创建
}
//解锁
_mtx.unlock();
}
return _ins;
}
private:
Singleton() {} //设置为私有,防止类外实例化对象
Singleton(const Singleton& stn) = delete;
Singleton& operator=(const Singleton& stn) = delete;
vector<string> _v;
static mutex _mtx;
static Singleton* _ins; //定义静态Singleton指针
};
Singleton* Singleton::_ins = nullptr;
mutex Singleton::_mtx; //初始化互斥锁
双检查加锁:
- 第一次 if 判断是为了提高效率,避免多个线程在访问这块代码的时候,多次进行上锁解锁操作
- 第二个 if 判断是为了保证线程安全,只进行一次资源的申请
正是因为线程安全问题,懒汉模式对单例的初始化就变得复杂起来(对比饿汉模式)
懒汉模式简化版本(C++11)
前提声明:简化版本的懒汉模式只适合C++11,C++11之前使用这个版本的懒汉模式不能保证线程安全问题
C++11之后,局部的静态成员变量在进行初始化,当其他线程来获取时会进入阻塞状态,直到初始化成功,从而保证了线程安全问题。
下面来实现一个简化版本的懒汉模式:
实现不需要用到互斥锁,也不需要定义静态的单例对象的指针。在静态成员函数中定义一个静态的单例对象,使用的时候,返回这个静态的单例对象地址即可(当然也可以返回这个对象的引用,看个人的喜欢)
class Singleton
{
public:
~Singleton()
{
cout << "~Singleton()" << endl;
}
static Singleton* GetInstance()
{
//懒汉模式的简化实现
static Singleton inst;
return &inst; //返回这个对象的地址
}
//...其他成员函数
private:
Singleton() {}
Singleton(const Singleton& stn) = delete;
Singleton& operator=(const Singleton& stn) = delete;
vector<string> _v;
};
int main()
{
Singleton* sln = Singleton::GetInstance();
return 0;
}
局部的静态变量有一个特点:只会初始化一次
局部的静态变量只有当程序运行到才会初始化;全局的静态变量在程序运行起来之前就进行初始化
单例释放问题
一般情况下,全局都要使用单例对象,所以单例对象不需要显示的释放。程序结束,资源也就被回收了。
但是有些特殊情况还是会考虑单例对象资源释放的。可以提供一个静态成员函数去调用析构函数;
当然,如果怕内存泄漏的话可以提供一个内部类。在单例类中定义内部类的对象,当内部类对象生命周期结束后,析构函数去调用单例的提供的释放资源的函数。从而达到单例对象指针的资源释放的功能。
具体可以参考以下代码:
class Singleton
{
public:
static Singleton* GetInstance()
{
// 双检查加锁
if (_ins == nullptr)
{
_imtx.lock();
if (_ins == nullptr)
{
_ins = new Singleton;
}
_imtx.unlock();
}
return _ins;
}
static void DelInstance() //释放资源的函数
{
_imtx.lock();
if (_ins)
{
delete _ins;
_ins = nullptr;
}
_imtx.unlock();
}
// 内部类:单例对象回收
class GC
{
public:
~GC()
{
DelInstance();
}
};
~Singleton()
{
// 持久化
// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
}
private:
// 限制类外面随意创建对象
Singleton()
{}
// 防拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
private:
vector<string> _v;
static GC _gc;
static Singleton* _ins;
static mutex _imtx;
};
单例模式就介绍到这里,下面来介绍一下C++的类型转换:
C++强制类型转换
在 C语言 中如果遇到 赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化。
C语言的类型类型转换可以分成两种:隐式类型转换 和 强制类型转换
- 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
- 显式类型转化:需要用户自己处理
C语言的隐式类型转换有时候会造成一些莫名的 BUG,来看这样的一段代码:
假设这段代码是完成数组的插入功能
void Insert(size_t pos, char ch)
{
int end = _size; // _size是数组个数
while(end >= pos)
{
//...
--end;
}
}
上面这段代码中,乍一看好像没有什么问题,指定 pos 位置进行字符的插入。
但是,仔细一点可以看到 end 变量是 int 类型,pos 是 size_t 类型。在进行大小比较的时候就会发生类型转化,end 变量会转换为 size_t 类型。
一般的,pos是大于0的数还好,一旦 pos为 0 时,end 是无符号整型,当end减到0时,循环体再进行end-- 操作,end 会变成负1吗?不会,end 会变成一个很大的无符号整数。程序就进入了死循环
像上面隐式类型转换很难去察觉。
C语言的显式类型转换将所有情况混合在一起,也会造成代码不够清晰。
面对C语言类型转换出现的缺点,C++要说将C语言这套类型转换全部丢掉是不可能的,因为C++要兼容C语言。
对此,为了规范类型转换,C++搞出了属于自己的一套类型转换方式。
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast
、reinterpret_cast
、const_cast
、dynamic_cast
static_cast 静态转换
static_cast
用于非多态类型的转换(静态转换),static_cast
它不能用于两个不相关的类型进行转换
示例:
int main()
{
//静态转换
double b = 11.22;
int a = static_cast<int>(b); //a、b变量为相似类型可以进行转换
cout << a << endl;
return 0;
}
如果两个不相近的类型使用 static_cast
会怎么样?
int main()
{
//静态转换
double b = 11.22;
int a = static_cast<int>(b); //a、b变量为相似类型可以进行转换
int *p = static_cast<int*>(a);//p是指针、a是整形类型不相近
return 0;
}
编译器会直接报错:
但是使用C语言的强制类型转换就可以:
int main()
{
//静态转换
double b = 11.22;
int a = static_cast<int>(b); //a、b变量为相似类型可以进行转换
cout << a << endl;
int* p = (int*)a; //强制类型转换
cout << p << endl;
return 0;
}
使用C++的类型转换更能保证数据的安全性。C++的类型转换只是一个规范,并不是强制性的。不是强制性的就会造成有人会遵守,有人不会遵守。
reinterpret_cast 不同类型转换
reinterpret_cast
用于将一种类型转换为另一种不同的类型(不同类型之间的强制转换)
示例:
int main()
{
//不同类型的转换
int c = 20;
int* p1 = reinterpret_cast<int*>(c);
cout << p1 << endl;
return 0;
}
reinterpret_cast
后面跟的尖括号表示:要将变量转化为怎么样的类型
const_cast 去除变量的const属性
const_cast
用途就是删除变量的const属性,方便赋值
示例:
int main()
{
//去除常量的属性
const int x = 10;
int* p = const_cast<int*>(&x);
*p = 20;
cout << x << " " << *p << endl;
return 0;
}
运行前来猜一下,x 和 *p 的值是什么?
这是编译器的优化问题,x的值是直接在寄存器中获取的,*p 是在内存中获取的。寄存器的运行速度是远高于内存的,并不能说 x的值没有修改,而是修改了还没有来得及更新。可以使用 volatile
关键字,让 x 不要去寄存器中获取值。
int main()
{
//去除常量的属性
volatile const int x = 10;//volatile不让编译器去寄存器拿数据
int* p = const_cast<int*>(&x);
*p = 20;
cout << x << " " << *p << endl;
return 0;
}
此时再来看看打印的结果:
在使用 const_cast
的时候要注意,后跟尖括号要转换为对应的指针类型,不然编译器会报错
int main()
{
//去除常量的属性
volatile const int x = 10;
int p = const_cast<int>(x); //error
p = 20;
cout << x << " " << p << endl;
return 0;
}
dynamic_cast 动态转换
dynamic_cast
用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)
前提声明:父类表示上,子类表示下
在这里引入两个词:向上转型 和 向下转型
- 向上转型:子类对象指针/引用 转换为 父类指针/引用(不需要转换,赋值兼容规则)
对向上转型不太理解,或者说对继承还不熟悉的小伙伴可以转站看看小编写的这篇文章:C++继承介绍,直接跳转到文章的 基类和派生类对象之间的赋值 进行查阅。
- 向下转型:父类对象指针/引用 转换为 子类指针/引用(用dynamic_cast转型)
注意:dynamic_cast
只能用于父类含有虚函数的类,dynamic_cast
转换成功会返回这个子类的指针/引用;转换失败会返回0。
示例:
class A
{
public:
virtual void f(){}
};
class B : public A
{};
void fun(A* pa, const string& s)
{
cout << "pa指向" << s << endl;
B* pb = dynamic_cast<B*>(pa);
}
int main()
{
A a;
B b;
fun(&a, "指向父类的指针");
fun(&b, "指向子类的指针");
return 0;
}
实现一个简单的继承体系(B 继承 A,A实现了虚函数),通过 fun 函数的参数 pa 父类的指针,对被传入的参数进行 向上转型操作。然后在 fun 函数内部实现 向下转型(利用 dynamic_cast
将pa 父类的指针转换为 pb 子类的指针)
如果 父类对象的 指针(或者是引用)原先就是指向派生类的,在使用 dynamic_cast
进行动态转换可以转换成功;
如果 父类对象的 指针(或者是引用)原先指向的是基类的,在使用 dynamic_cast
进行动态转换会转换失败
没错,使用 dynamic_cast
动态转换是有条件的。同样的,使用C语言的强制类型转化也可以:
class A
{
public:
virtual void f(){}
};
class B : public A
{};
void fun(A* pa, const string& s)
{
cout << "pa指向" << s << endl;
//强制转换
B* pb1 = (B*)pa;
cout << "[强制转换]:pb1:" << pb1 << endl;
//动态转换
B* pb2 = dynamic_cast<B*>(pa);
cout << "[dynamic_cast转换]:pb2:" << pb2 << endl;
}
int main()
{
A a;
B b;
fun(&a, "指向父类的指针");
fun(&b, "指向子类的指针");
return 0;
}
强制转换不会管 pa 原先指向的是父类还是子类,我都给你转换,然后返回一个地址。这个地址可以使用吗?答案是不可以的,会造成越界访问。不相信的老铁可以试试,在这里就不演示了。
使用C语言的强制类型转换是不安全的,使用C++的 dynamic_cast 动态转换是安全的
为了代码的可视性,建议大家还是使用C++的强制类型转换的方式。虽然不是强制性的,但是可以避免很多的潜在 bug 发生。
这篇文章就介绍到这里,感谢大家的观看!