C++ 之 【特殊类设计 与 类型转换】
目录
1. 特殊类设计
1.1 不能被拷贝的类
1.2 只能在堆上创建对象的类
1.3 只能在栈上创建对象的类
1.4 不能被继承的类
1.5 只能创建一个对象的类(单例模式)
饿汉模式
懒汉模式
2. 类型转换
2.1 C语言中的类型转换
2.2 为什么C++需要四种类型转换
2.3 C++强制类型转换
static_cast
reinterpret_cast
const_cast
dynamic_cast
3. RTTI(了解)
1. 特殊类设计
1.1 不能被拷贝的类
不能被拷贝,则不能被拷贝构造或赋值
class A
{ //...A(const A& aa) = delete;A& operator=(const A& aa) = delete;//...
};class A
{ //...
private:A(const A& aa);A& operator=(const A& aa);//...
};
为了禁用 拷贝构造和赋值重载函数
C++11 使用delete让编译器删除掉该默认成员函数
C++98 将这两个成员函数声明为私有(不定义可以防止成员函数内部进行拷贝操作)
1.2 只能在堆上创建对象的类
//只能再堆上创建对象
class HeapOnly
{
public:static HeapOnly* CreateObj(){return new HeapOnly;}private:HeapOnly(){ // ...}HeapOnly(const HeapOnly& ho){ // ... }
};
(1) 私有构造函数、拷贝构造函数,使得外界不能直接创建对象
(2) 提供一个静态成员函数返回在堆上创建对象的指针,外界再用指针接收即可
不能使用成员函数返回在堆上创建对象的指针,
因为成员函数需要被对象调用,而此时还没有对象
1.3 只能在栈上创建对象的类
class StackOnly
{
public:static StackOnly CreateObj(){return StackOnly();}//删除全局 operator new,禁止 new 分配// StackOnly obj = StackOnly::CreateObj();// StackOnly* ptr3 = new StackOnly(obj);void* operator new(size_t size) = delete;
private:StackOnly(){}
};
(1) 私有构造函数,使得外界不能直接创建对象
(2) 提供一个静态成员函数返回在栈上创建的对象
不能传指针或引用返回,因为函数结束对象就销毁了 ;不能使用成员函数返回在栈上创建的对象 ,因为成员函数需要被对象调用,而此时还没有对象
(3) 删除全局 operator new,禁止调用拷贝构造函数,详见 内存管理 new的原理
1.4 不能被继承的类
//设计一个不能被继承的类
class C final
{};
class B
{
public:static B GetInstance(){return B();}private:B(){}
};
C++11中 用 final 修饰,则该类不能被继承
C++98中 将构造函数私有,派生类不能调用基类的构造函数,就不能实现继承
1.5 只能创建一个对象的类(单例模式)
设计模式是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的 总结
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样
单例模式: 一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享
单例模式有 饿汉模式和懒汉模式 之分
-
饿汉模式
namespace hungry
{class Singleton{public:static Singleton* CreateObj(){return &_sl;}private:Singleton() {}//私有构造函数,外界不能创建Singleton(const Singleton& sl);//私有拷贝构造函数,外界不能创建Singleton& operator=(const Singleton& sl);//防止拷贝static Singleton _sl;};Singleton Singleton::_sl;//在程序入口之前就完成单例对象的初始化
}
(1) 私有构造函数,使得外界不能直接创建对象
(2) 私有拷贝构造函数、赋值重载函数,且不实现,防止外界与成员函数内部进行拷贝
(3) 声明一个静态成员对象,整个类只有这一个对象,并且是静态的
(4) 提供一个静态成员函数返回静态成员对象的地址
饿汉模式
优点:简单,程序启动时就创建一个唯一的实例对象
缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定
-
懒汉模式
暂时未涉及多线程
namespace lazy
{class Singleton{public:static Singleton* CreateObj(){// 注意这里一定要使用Double-Check的方式加锁,才能保证效率和线程安全if (!_sl)_sl = new Singleton();return _sl;}// 实现一个内嵌垃圾回收类 class CGarbo //类似于智能指针{public:~CGarbo(){if (Singleton::_sl)delete Singleton::_sl;}};static CGarbo Garbo;private:Singleton() {}//私有构造函数,外界不能创建Singleton(const Singleton& sl);//私有拷贝构造函数,外界不能创建Singleton& operator=(const Singleton& sl);//防止拷贝static Singleton* _sl;//单例对象指针};Singleton* Singleton::_sl = nullptr;Singleton::CGarbo Garbo;
}
(1) 私有构造函数,使得外界不能直接创建对象
(2) 私有拷贝构造函数、赋值重载函数,且不实现,防止外界与成员函数内部进行拷贝
(3) 声明一个静态成员指针(整个类共用),初始化为空,后续指向唯一的对象
(4) 提供一个静态成员函数返回单例对象指针
(5) 单例类对象的析构函数不能被直接调用,即使指针调用也有内存泄露的风险,解决方法:
实现一个内部类,并在单例类中声明一个静态内部类对象,当单例类对象生命周期结束时,编译器调用单例类对象的析构函数的同时,也会调用内部类对象的析构函数,进而释放单例类对象,类似于智能指针
多线程实现方式,后续讲解
class Singleton
{
public:static Singleton* GetInstance() {// 注意这里一定要使用Double-Check的方式加锁,才能保证效率和线程安全if (nullptr == m_pInstance) {m_mtx.lock();if (nullptr == m_pInstance) {m_pInstance = new Singleton();}m_mtx.unlock();}return m_pInstance;}// 实现一个内嵌垃圾回收类 class CGarbo {public:~CGarbo() {if (Singleton::m_pInstance)delete Singleton::m_pInstance;}};// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象static CGarbo Garbo;
private:// 构造函数私有Singleton() {};// 防拷贝Singleton(Singleton const&);Singleton& operator=(Singleton const&);static Singleton* m_pInstance; // 单例对象指针static mutex m_mtx;//互斥锁
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mtx;
2. 类型转换
2.1 C语言中的类型转换
C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换
1. 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败
2. 显式类型转化:需要用户自己处理
void Test()
{int i = 1;// 隐式类型转换double d = i;printf("%d, %.2f\n", i, d);int* p = &i;// 显示的强制类型转换int address = (int)p;printf("%x, %d\n", p, address);
}
缺陷: 转换的可视性比较差,所有的转换形式都是以同一种形式书写,难以跟踪错误的转换
2.2 为什么C++需要四种类型转换
C风格的转换格式很简单,但是有不少缺点的:
1. 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
2. 显式类型转换将所有情况混合在一起,代码不够清晰
因此C++提出了自己的类型转化风格,
注意因为C++要兼容C语言,所以C++中还可以使用C语言的 转化风格
2.3 C++强制类型转换
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:static_cast、reinterpret_cast、const_cast、dynamic_cast
-
static_cast
static_cast用于非多态类型的转换(静态转换),
编译器隐式执行的任何类型转换都可用 static_cast,即
static_cast用于两个相关的类型进行转换
int main()
{double d = 12.34;int a = static_cast<int>(d);cout << a << endl;return 0;
}
-
reinterpret_cast
reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,
reinterpret_cast用于将一种类型转换为另一种不同的类型
int main()
{double d = 12.34;int a = static_cast<int>(d);cout << a << endl;// 这里使用static_cast会报错,应该使用reinterpret_cast//int *p = static_cast<int*>(a);int* p = reinterpret_cast<int*>(a);return 0;
}
-
const_cast
const_cast最常用的用途就是删除变量的const属性,方便赋值
const_cast 不能直接用于基本类型(如 int)的转换,需通过指针或引用间接操作
void Test()
{const int a = 2;int* p = const_cast<int*>(&a);*p = 3;cout << a << endl;
}
通过指针p我们确实修改了常变量a在内存中存储的值,
但是,编译器会优化常变量的访问操作,即上述代码打印出 a 的值为 2
要想真正使用常变量内存中存储的值需要使用 volatile 关键字
volatile 是 C++ 中的一个关键字,用于告知编译器某个变量的值可能在程序外部被意外修改
void Test()
{volatile const int a = 2;//常变量int* p = const_cast<int*>(&a);*p = 3;cout << a << endl;
}
-
dynamic_cast
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)
向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)
向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)
注意:
1. dynamic_cast只能用于父类含有虚函数的类
2. dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0
class A
{
public:virtual void f() {}
};
class B : public A
{
};
void fun(A* pa)
{//父只有是指向儿子的时候,才能进行转换// dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回B* pb1 = static_cast<B*>(pa);B* pb2 = dynamic_cast<B*>(pa);cout << "pb1:" << pb1 << endl;cout << "pb2:" << pb2 << endl;
}
int main()
{A a;B b;fun(&a);fun(&b);return 0;
}
父类指针或引用指向子类对象时,父类指针或引用转换为子类指针或引用才是安全的
否则,可能会造成父类指针或引用的越界访问,进而导致未定义行为
注意
强制类型转换关闭或挂起了正常的类型检查,每次使用强制类型转换前,程序员应该仔细考虑是 否还有其他不同的方法达到同一目的,如果非强制类型转换不可,则应限制强制转换值的作用 域,以减少发生错误的机会。
强烈建议:避免使用强制类型转换
3. RTTI(了解)
RTTI:Run-time Type identification的简称,即:运行时类型识别。
C++通过以下方式来支持RTTI:
1. typeid运算符
2. dynamic_cast运算符
3. decltype