【C++】特殊类设计、单例模式与类型转换
目录
一、设计一个类不能被拷贝
(一)C++98
(二)C++11
二、设计一个类只能在堆上创建对象
(一)将构造函数私有化,对外提供接口
(二)将析构函数私有化
三、设计一个类只能在栈上创建对象
四、设计一个类不能被继承
(一)C++98
(二)C++11
五、设计一个类只能创建一个对象(单例模式)
(一)饿汉模式
(二)懒汉模式
六、类型转换
(一)C语言类型转换
(二)C++新增四种强制类型转换
1、static_cast
2、reinterpret_cast
3、const_cast
4、dynamic_cast
一、设计一个类不能被拷贝
(一)C++98
class banCopy
{
private:
banCopy(const banCopy& bc);
banCopy& operator=(const banCopy& bc);
};
通过将拷贝构造以及赋值重载私有化使得外部不能调用拷贝构造以及赋值重载。但是对于内部而言仍可以进行类的拷贝。
(二)C++11
class banCopy
{
banCopy(const banCopy& bc) = delete;
banCopy& operator=(const banCopy& bc) = delete;
};
通过使用关键字直接删除相关函数。
二、设计一个类只能在堆上创建对象
(一)将构造函数私有化,对外提供接口
class heapOnly
{
public:
static heapOnly* createObj()
{
return new heapOnly;
}
private:
heapOnly() {};
heapOnly(const heapOnly& h) = delete;
heapOnly& operator = (const heapOnly& h) = delete;
};
首先将构造函数私有化,可以禁止在栈帧以及静态区创建对象,专门提供创建堆上对象的接口。同时也需要将接口设置为静态成员函数,使得无需对象也可以直接调用该接口。
heapOnly* ph = heapOnly::createObj();
需要注意的是,也要将拷贝构造与赋值重载删除掉,如果不进行删除,仍然可以通过拷贝对象生成在栈帧或者静态区的对象。
//若不禁用拷贝函数,即可通过拷贝仍在栈帧上生成对象
heapOnly* ph = heapOnly::createObj();
heapOnly h(*ph);
(二)将析构函数私有化
class heapOnly
{
public:
heapOnly() { };
void destoy()
{
this->~heapOnly();
}
private:
~heapOnly() { };
};
利用类与对象的特性:局部对象出作用域会自动调用析构函数释放资源的特性,将析构函数私有化使得在栈上或静态区上开辟的对象会因此导致编译错误,只能在堆上开辟对象。
因为在堆上开辟的对象需要手动 delete 释放资源,而使用 delete 关键字仍然会调用析构函数(私有化)而导致编译报错,因此可以提供释放资源的接口来实现资源的释放。
三、设计一个类只能在栈上创建对象
class stackOnly
{
public:
static stackOnly createObj()
{
return stackOnly();
}
private:
stackOnly() { }
};
通过构造函数私有化并提供创建对象接口实现只在栈帧上开辟空间。因此使用 new 生成对象会自动调用构造函数,又因构造函数被私有化了因此使用 new 关键字生成对象会编译报错,故禁止了在堆上开辟空间。
但上述方法并不能禁用从静态区生成对象,仍然可以通过拷贝在静态区生成对象。
static stackOnly so = stackOnly::createObj();
如果想彻底解决以上问题则需要禁用拷贝构造,但是如果禁用了拷贝构造,就不能从 createObj() 返回生成对象了,只能生成临时对象或是临时对象的引用,但是不能修改。
stackOnly::createObj(); //临时对象
const stackOnly& so = stackOnly::createObj(); //临时对象的引用
四、设计一个类不能被继承
(一)C++98
class finalClass
{
public:
static finalClass CreateObj()
{
return finalClass();
}
private:
finalClass() { }
};
因为子类构造函数会自动调用父类构造函数完成对父类变量的初始化,通过将父类构造函数私有化,使得子类无法调用父类的构造函数,因此使得该类无法被继承。
(二)C++11
class FinalClass final
{
};
C++11直接使用 final 关键字使得该类无法被继承。
五、设计一个类只能创建一个对象(单例模式)
(一)饿汉模式
class InfoSingleton
{
public:
//返回私有静态成员
static InfoSingleton& GetInstance()
{
return _sins;
}
void insert(string name, int salary)
{
_um[name] = salary;
}
void Print()
{
for (auto& e : _um)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
}
private:
//构造函数私有化
InfoSingleton(){}
//删除拷贝构造以防新建对象
InfoSingleton(const InfoSingleton& sins) = delete;
InfoSingleton& operator=(const InfoSingleton& sins) = delete;
//示例功能 无特殊含义
unordered_map<string, int> _um;
//定义静态类型成员
static InfoSingleton _sins;
};
InfoSingleton InfoSingleton::_sins; //初始化静态变量
int main()
{
//下列代码可证明返回的是同一个对象
InfoSingleton& sins1 = InfoSingleton::GetInstance();
sins1.insert("张三", 15000);
sins1.insert("李四", 20000);
sins1.insert("王五", 10000);
sins1.insert("赵六", 30000);
sins1.Print();
InfoSingleton& sins2 = InfoSingleton::GetInstance();
sins2.insert("赵六", 20000);
sins2.Print();
return 0;
}
以上是利用所有对象公用一个类静态成员的特性,通过私有构造函数以及删除拷贝函数给出静态类型成员变量的静态接口函数,使得该类只能通过该接口返回静态成员对象。
因为静态成员变量需要在类外进行初始化,而在初始化的同时会自动调用构造函数进行初始化。因此该静态成员对象的初始化,也就是调用构造函数会在main函数之前。
InfoSingleton InfoSingleton::_sins; //初始化静态变量
饿汉模式的特点:
1、 单例对象初始化时,数据太多会导致启动慢;
2、如果多个单例类有初始化的依赖关系,饿汉模式无法控制。例如A和B都是单例类,因为B的启动依赖A,所以需要先初始化A,再初始化B,但是饿汉模式无法控制对象的初始化顺序;
3、饿汉模式创建的对象不会有线程安全问题,因为该模式的对象在main函数之前已经被创建好了,而mian函数之前线程都没启动。
(二)懒汉模式
class InfoSingleton
{
public:
//返回私有静态成员
static InfoSingleton& GetInstance()
{
if (_sins == nullptr)
{
_sins = new InfoSingleton;
}
return *_sins;
}
void insert(string name, int salary)
{
_um[name] = salary;
}
void Print()
{
for (auto& e : _um)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
}
private:
//构造函数私有化
InfoSingleton(){}
//删除拷贝构造以防新建对象
InfoSingleton(const InfoSingleton& sins) = delete;
InfoSingleton& operator=(const InfoSingleton& sins) = delete;
//示例功能 无特殊含义
unordered_map<string, int> _um;
//定义静态类型成员
static InfoSingleton* _sins;
};
InfoSingleton* InfoSingleton::_sins = nullptr; //初始化静态变量
int main()
{
//下列代码可证明返回的是同一个对象
InfoSingleton& sins1 = InfoSingleton::GetInstance();
sins1.insert("张三", 15000);
sins1.insert("李四", 20000);
sins1.insert("王五", 10000);
sins1.insert("赵六", 30000);
sins1.Print();
InfoSingleton& sins2 = InfoSingleton::GetInstance();
sins2.insert("赵六", 20000);
sins2.Print();
return 0;
}
同样的,私有构造函数以及删除拷贝函数并提供生成对象接口都是为了单例模式做准备,与饿汉模式类同。
与饿汉模式不同的是,懒汉模式封装的是静态对象指针,也就是在 createObj() 函数第一次进入时会在堆区新建一个对象,而之后的进入则是返回该对象,以此实现只能生成一个对象。
但是以上的代码存在线程安全问题,多个线程进入 createObj() 函数可能会导致多个线程都会在堆上新建对象导致内存泄漏,因此需要对以上接口进行修改。
class InfoSingleton
{
public:
//返回私有静态成员
static InfoSingleton& GetInstance()
{
if (_sins == nullptr)
{
_mutex.lock();
if (_sins == nullptr)
{
_sins = new InfoSingleton;
}
_mutex.unlock();
}
return *_sins;
}
private:
static mutex _mutex;
};
将 _sins 的检查以及赋值进行加锁保护,这里进行了两层 if 进行条件判断。
如果只有内部一层 if 判断,会导致每个线程进入该函数都会先加锁,进行判断操作后再解决,降低了系统性能。
在外层再加一个 if 判断,当第一个线程进入时正常加锁判断操作,但是当之后的线程进入以后,再遇到第一个 if 判断后因为此时 _sins 不为空而无需进行加锁判断了,直接返回 _sins 即可,提高了性能效率。
懒汉模式的特点:
1、对象在main函数之后才会创建;
2、可以主动控制对象的创建时机。
3、创建对象时存在线程安全问题,因此需要用户进行保护控制。
六、类型转换
(一)C语言类型转换
在C语言中,如果赋值运算符两边类型不同,或是函数形参实参类型不同,或是函数返回值和接收值类型不同时等情况都会发生类型转换。C语言中共有两种形式的类型转换:显式类型转换或是隐式类型转换。
int main()
{
int a = 0;
//隐式类型转换
double b = a;
int* pa = &a;
//显示类型转换
int c = (int)pa;
return 0;
}
C语言的类型转换可能会存在一些问题,例如精度丢失或是隐式转换可能带来一些隐藏的bug等,因此C++提出了新的类型转换方式。
(二)C++新增四种强制类型转换
1、static_cast
static_cast 常用于非多态类型的转换,在C语言中能隐式转换的都可以使用 static_cast 进行转换,其主要用于相近类型转换。
int main()
{
double a = 1.1;
int b = static_cast<int>(a);
std::cout << b << std::endl;
return 0;
}
2、reinterpret_cast
reinterpret_cast 用于将一种类型转换为另一种不同的类型,常用于不相关类型的转换。
int main()
{
int a = 1;
int* pa = &a;
//这里如果使用 static_cast 会编译报错
int b = reinterpret_cast<int>(pa);
std::cout << b << std::endl;
return 0;
}
3、const_cast
const_cast 用于对具有 const 属性的变量进行转换,常用于删除变量的const属性,方便赋值。
int main()
{
//volatile const int a = 1;
const int a = 1;
int* b = const_cast<int*>(&a);
++(*b);
std::cout << a << std::endl << *b << std::endl;
return 0;
}
以上代码运行结果为 1 2,使用const_cast 取消了变量 a 的常量属性,但指针 b 仍然指向变量 a 在内存中的位置,因此可以通过指针 b 修改内存中 a 的值。
对于最后的运行结果在不同平台下的运行结果可能不同,因为编译器对 const 变量会有优化,认为 const 变量在运行中不会改变,因此将 const 变量存入寄存器或告诉缓存中,方便访问读取。而通过指针 b 修改的值实际是修改了于内存中的值,因此打印结果不同。
可以使用 volatile 关键字保持内存可见性,编译器将取消以上优化仍从内存读取 const 变量,因此加入关键字后打印输出的值相同。
4、dynamic_cast
以上三种类型转换实际在C语言中已经存在,只是将其更加规范化。
dynamic_cast 用于将一个父类的指针或引用转换为子类对象的指针或引用。
向上转型:子类对象指针或引用 -> 父类对象指针或引用 (无需进行转换,发生切片)
向下转型:父类对象指针或引用 -> 子类对象指针或引用 (使用 dynamic_cast 进行类型转换)
需要注意的是:dynamic_cast 只能用于父类含有虚函数的类,如果转换失败会返回0。
class A
{
public:
virtual void func() { }
};
class B : public A
{
};
void function(A* pa)
{
B* pb1 = static_cast<B*>(pa);
B* pb2 = dynamic_cast<B*>(pa);
std::cout << "pb1:" << pb1 << std::endl;
std::cout << "pb2:" << pb2 << std::endl;
}
int main()
{
A a;
B b;
function(&a);
function(&b);
return 0;
}
以下是上述代码的运行结果,我们来简要分析以下:
实际上 function() 函数是一个多态调用。如果传入的是类型为 B 的对象,无论是 static_cast 还是 dynamic_cast 转换都可以成功,因为函数参数指针 pa 本身指向的就是类型为 B 的对象,因此可以正常使用转换后的指针。
但是当传入的是类型为 A 的对象, 我们可以从图可知, static_cast 转换成功了但是 dynamic_cast 转换失败指向为0,而指向为0这也符合 dynamic_cast 的特性。虽然 static_cast 转换成功了,但是在使用该转换后的指针存在着极大的风险会造成未定义行为。
在以上场景中,相较于 static_cast, dynamic_cast 相当于多了一层检查,如果不能确保能够安全转换,那不建议使用 static_cast 进行强制类型转换。