特殊类的设计和类型转换
特殊类的设计
1.设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
//(1)C++98:将拷贝构造函数与赋值运算符重载只声明不定义并且将其访问权限设置为私有即可。
class A
{
public:
A(){}
private:
A(A&);
A& operator=(const A&);
int x;
};
//(2)C++11:扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上
//= delete,表示让编译器删除掉该默认成员函数。
class B
{
public:
B() {}
B(B&) = delete;
B& operator=(const B&) = delete;
int x;
};
2.设计一个类,只能在堆上创建对象
两种实现方式:
- 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。
- 析构函数私有化,提供destory接口释放空间。
//只能在堆上开空间
// 第一种方案:构造、拷贝构造私有化,提高static返回创建对象指针
class A {
public:
static A* get()
{
return new A;
}
private:
A(A&){}
A(){}
};
//第二种方案:析构函数私有化,提供destory接口释放空间
class B {
public:
void Destory()
{
delete this;
}
private:
~B() //栈上变量函数调用结束前调不动析构
{
cout << "~B" << endl;
}
};
3.设计一个类,只能在栈上创建对象
将构造函数私有化,然后设计静态方法创建对象返回即可
//设计一个类,只能在栈上面开空间
//禁用new,设计static方法返回局部对象
class C {
public:
static C get()
{
return C();
}
private:
C(){}
void* operator new(size_t s) = delete;
};
4.设计一个类,不能被继承
//不能被继承
// (1)C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class A
{
public:
static A GetInstance()
{
return A();
}
private:
A(){}
};
//(2)C++11方法可用final关键字,final修饰类,表示该类不能被继承。
class B final
{
///
};
5.单例模式
单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
两种设计:
- 饿汉模式:在main函数执行前就创建好。
//单例化模式的设计
//饿汉模式:在main函数前创建好
//要点:(1)只能右一个实例,把构造和拷贝构造私有
//(2)要在main函数前就创建好,我们可以设计成静态成员,类似与全局变量
//(3)提供全局函数返回对象引用或指针
//优点:简单
//缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。
class A {
public:
static A& get()
{
return a;
}
//需要什么变量和方法自己添加
private:
A() {};
A(A&) {};
A& operator=(A&) = delete;//赋值最好禁止掉,但自己给自己赋值也影响不大
static A a;
};
A A::a; //类外定义
- 懒汉模式:需要使用的使用才创建。
//懒汉模式:需要的时候创建
//要点:(1)只能有一个实例,把构造和拷贝构造私有
//(2)设计一个静态变量指针,初始化为空
//(3)第一次调用get方法的时候才创建对象
class B {
public:
static B* get()
{
if (b == nullptr)
{
b = new B();
}
return b;
}
private:
B() {};
B(B&) {};
B& operator=(B&) = delete;
static B* b;
};
B* B::b = nullptr;
//如果需要在退出时进行数据持久化,可以利用析构函数和内部类
//可以手动调用,但不调也会在main结束前自动调用
class B {
public:
static B* get()
{
if (b == nullptr)
{
b = new B();
}
return b;
}
static void destory()
{
if (b) {
//数据持久化操作
delete b; b = nullptr; //懒汉释放空间其实不重要,重要的是可以在这个过程进行数据持久化
cout << "destory" << endl;
}
}
private:
B() {};
B(B&) {};
B& operator=(B&) = delete;
static B* b;
class C {
public:
~C()
{
B::destory();
}
};
static C c; //在main函数结束前会调用相应的析构函数
};
B* B::b = nullptr;
B::C B::c;
C++的类型转换
1. C语言中的类型转换
在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型转换和显式类型转换。
- 隐式类型:编译器在编译阶段自动进行,能转就转,不能转就编译失败
- 显式类型转化:需要用户自己处理
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.C语言类型转换的缺点
- 隐式类型转化有些情况下可能会出问题:比如数据精度丢失
- 显式类型转换将所有情况混合在一起,代码不够清晰
因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格。
3.C++的强制类型转换
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
- static_cast
int main()
{
//相近类型(意义也相近)的转化(对应C的int、double、char之间转换)
double d = 12.34;
int a = static_cast<int>(d);
cout<<a<<endl;
return 0;
}
- reinterpret_cast
int main()
{
//reinterpret_cast用于有一定关联,但意义不相似的类型间转换(对应C的int与int*)
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
void Test()
{
//const_cast最常用的用途就是删除变量的const属性,方便赋值
//注意:const_cast属于比较危险的转换
volatile const int a = 2;
int* p = const_cast<int*>(&a);
*p = 3;
//这里加volatile是因为编译器对const变量有优化,可能会放到寄存器,也有可能是把a直接替换为2
//所以有时内存中数据修改了,但是打印发现没有变化,加volatile可以强制每次都去内存取
cout << a << endl;
}
- dynamic_cast
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(向下转型,动态转换)
- 向上转型:子类对象指针 / 引用->父类指针 / 引用(不需要转换,赋值兼容规则)
- 向下转型:父类对象指针 / 引用->子类指针 / 引用(用dynamic_cast转型是安全的)
使用注意:
-
dynamic_cast只能用于父类含有虚函数的类(虚函数我在多态那一文讲过)
为什么:dynamic_cast 的工作原理是基于运行时的类型信息(RTTI)。当一个类包含至少一个虚函数时,编译器会自动为该类生成一个虚函数表,其中包含了所有虚函数的地址。每个该类的对象都会存储一个指向虚函数表的指针。因此,通过检查这个指针,我们可以确定对象的实际类型。但是,如果一个类没有虚函数,那么它就不会有虚函数表,也就无法在运行时确定其实际类型。在这种情况下,使用 dynamic_cast 进行类型转换会导致未定义的行为。 ---- 简而言之就是不知道指针指向的类型,不能确保安全性。 -
dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回空。
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;
}
C++中const引用做参数的特殊机制
先看一种常见情况:
void fun(vector<int> v)
{
//不做操作
}
int main()
{
initializer_list<int> li = { 1, 2, 3 };
//这里很明显,也很常见,是隐式类型转换
//只要vector<int>支持了initializer_list<int>做参数的构造即可
fun(li);
}
再看引用做参数:
void fun(vector<int>& v)
{
//不做操作
}
int main()
{
initializer_list<int> li = { 1, 2, 3 };
//这里会报错,也很好理解,引用底层是指针,initializer_list<int>* 和 vector<int>* 不支持隐式转换
fun(li);
}
最后看const引用做参数:
void fun(const vector<int>& v)
{
//不做操作
}
int main()
{
initializer_list<int> li = { 1, 2, 3 };
//这里不报错,原因是触发了隐式转换(存在对应构造函数),为什么:
//(1)const修饰后是不支持修改的,这个时候隐式转换是安全的
//(2)如果普通引用也支持隐式类型转换的话,可能修改关键数据造成错误
fun(li);
}
RTTI(扩展)
RTTI:Run-time Type identification的简称,即运行时类型识别。
C++通过以下方式来支持RTTI:
- typeid运算符
- dynamic_cast运算符(本质是检查虚函数表)
- decltype