第二十三讲:特殊类和类型转换
目录
1、特殊类设计
1.1、不能被拷贝
1.2、只能在堆上创建
1.3、只能在栈上创建
1.4、不能被继承
1.5、单例模式
1.5.1、饿汉模式
1.5.2、懒汉模式
2、类型转换
2.1、C语言中的类型转换
2.2、C++四种类型转换
2.2.1、static_cast
2.2.2、reinterpret_cast
2.2.3、const_cast
2.2.4、dynamic_cast
3、RTTI
1、特殊类设计
1.1、不能被拷贝
拷贝只会出现在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝, 只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98:将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。原因:
1、设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以拷贝了。
2、只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果声明了就不会生成默认的拷贝和赋值重载函数了。
C++11:在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
不再举例,因为在智能指针这一部分已经举例了。
1.2、只能在堆上创建
第一种方式是:将析构函数私有化。例如:
class HeapOnly
{
private:~HeapOnly(){cout << "~HeapOnly" << endl;}
};int main()
{HeapOnly hp1; // 会报错static HeapOnly hp2; // 会报错return 0;
}
这样就只能在堆上创建对象了,如果要释放在堆上创建的该对象,直接使用delete是不行的,可以写一个销毁函数来解决这个问题,如下:
class HeapOnly
{
public:static void Destory(HeapOnly* ptr){delete ptr;}
private:~HeapOnly(){cout << "~HeapOnly" << endl;}
};int main()
{ HeapOnly* ptr = new HeapOnly;HeapOnly::Destory(ptr);return 0;
}
或者,也可以像下面这样写:
class HeapOnly
{
public:void Destory(){delete this;}
private:~HeapOnly(){cout << "~HeapOnly" << endl;}
};int main()
{ HeapOnly* ptr = new HeapOnly;ptr->Destory();return 0;
}
第二种方式:将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝构造在栈上生成对象。例如:
class HeapOnly
{
private:HeapOnly(){cout << "HeapOnly" << endl;}HeapOnly(const HeapOnly& hp){cout << "HeapOnly(const HeapOnly& hp)" << endl;}
};int main()
{// 下面的这些都会报错HeapOnly hp1;static HeapOnly hp2;HeapOnly* ptr = new HeapOnly;HeapOnly copy(ptr);return 0;
}
如果我们此时想要创建对象的话,就可以像下面这样做:
class HeapOnly
{
public:static HeapOnly* CreateObj(){return new HeapOnly;}
private:HeapOnly(){cout << "HeapOnly" << endl;}HeapOnly(const HeapOnly& hp){cout << "HeapOnly(const HeapOnly& hp)" << endl;}
};int main()
{HeapOnly* ptr = HeapOnly::CreateObj();return 0;
}
1.3、只能在栈上创建
可以将构造函数私有化,并禁掉new和delete函数。例如:
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;void operator delete(void* p) = delete;private:StackOnly(){cout << "StackOnly" << endl;}
};int main()
{StackOnly obj = StackOnly::CreateObj();StackOnly* ptr = new StackOnly; // 报错StackOnly* ptr = new StackOnly(obj); // 报错return 0;
}
1.4、不能被继承
C++98:构造函数私有化,派生类中调不到基类的构造函数。则无法继承。
C++11:使用final关键字,final修饰类,表示该类不能被继承。
这个在之前就已经提到过了,不再举例。
1.5、单例模式
设计模式是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人与人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》,设计模式也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模 式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
注:设计模式有很多,常见的设计模式就几个,下面的单例模式就是常见的一种设计模式。
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享。单例模式有两种实现模式: 饿汉模式和懒汉模式。
1.5.1、饿汉模式
饿汉模式就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
例如:
// 饿汉模式
class A
{
public:static A* GetInstance(){return &_inst;}void Add(const string& key, const string& value){_dict[key] = value;}void Print(){for (auto kv : _dict){cout << kv.first << " : " << kv.second << endl;}cout << endl;}private:A(){}A(const A& aa) = delete;A& operator=(const A& aa) = delete;map<string, string> _dict;int _n = 0;static A _inst;
};
A A::_inst; // 在执行main函数前就会被创建好。int main()
{A::GetInstance()->Add("sort", "分类、排序");A::GetInstance()->Add("left", "左边");A::GetInstance()->Add("right", "右边");A::GetInstance()->Print();return 0;
}
优点:相比下面的懒汉模式实现会更加简单。
缺点:可能会导致进程启动变慢(指从程序启动到开始执行main函数);另外一个就是如果一个项目中有多个单例,这多个单例类对象实例启动顺序不确定,饿汉模式是无法控制实例化的顺序的,此时就需要考虑下面的懒汉模式了。
1.5.2、懒汉模式
如果单例对象构造十分耗时或者占用很多资源,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢;此外,如果有顺序的要求,也不能使用饿汉模式。所以在这些情况下使用懒汉模式更好。
例如:
// 懒汉模式
class B
{
public:static B* GetInstance(){if (_inst == nullptr){_inst = new B; // 懒汉对象一般不需要释放,因为程序运行过程中,还需要使用它(这种不属于内存泄漏)。// 等到进程正常结束,它也就自然而然的释放了。}return _inst;}void Add(const string& key, const string& value){_dict[key] = value;}void Print(){for (auto kv : _dict){cout << kv.first << " : " << kv.second << endl;}cout << endl;}static void DelInstance() // 如果有需要的话,可以调用该函数销毁懒汉对象。{if (_inst != nullptr){delete _inst;_inst = nullptr;}}private:B(){}~B(){// 如果需要做一些动作的话,比如:持久化(将一些数据写到文件)cout << "数据写到文件" << endl;}B(const B& aa) = delete;B& operator=(const B& aa) = delete;map<string, string> _dict;int _n = 0;static B* _inst;class gc{public:~gc(){DelInstance();}};static gc _gc;
};
B* B::_inst = nullptr; // 在执行main函数前就会被创建好。
B::gc B::_gc;int main()
{B::GetInstance()->Add("sort", "分类、排序");B::GetInstance()->Add("left", "左边");B::GetInstance()->Add("right", "右边");B::GetInstance()->Print();B::GetInstance()->Add("right", "xxx");B::GetInstance()->Print();return 0;
}
优点:第一次使用实例对象时,创建对象,进程启动无负载。多个单例实例启动顺序可自由控制。
缺点:实现比较复杂,懒汉模式是有线程安全问题的,等后面的文章讲线程时再说。
2、类型转换
2.1、C语言中的类型转换
在C语言中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与 接收返回值类型不一致时,就需要发生类型转化,C语言中总共有两种形式的类型转换:隐式类型 转换和显式类型转换。
1、隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
2、显式类型转化:需要用户自己处理。
例如:
void Test()
{int i = 1;// 隐式类型转换double d = i; // 因为int和double都是用来表示数据大小的,所以可以进行隐式类型转换。int* p = &i;// 显示的强制类型转换int address = (int)p; // 因为int和int*的含义是不一样的,一个表示地址,一个表示数据大小,因此不能隐式类型转换。// 之所以可以强制类型转化是因为两者之间还有一定的关联性。// 注:完全不相关的类型之间是无法进行互相转换的。
}
C风格的转换格式很简单,但是有不少缺点的:
1、隐式类型转化有些情况下可能会出问题:比如数据精度丢失。
2、显式类型转换代码不够清晰。
因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的 转化风格。
2.2、C++四种类型转换
标准C++为了加强类型转换的可视性以及可读性,引入了四种命名的强制类型转换操作符,如下:
2.2.1、static_cast
static_cast是静态转换,编译器隐式执行的任何类型转换都可用 static_cast,但它不能用于两个不相关的类型进行转换。对应的就是C语言的隐式类型转换。例如:
int main()
{double d = 12.34;int a = static_cast<int>(d);cout << a << endl;int* ptr = static_cast<int*>(a); // 会报错return 0;
}
2.2.2、reinterpret_cast
reinterpret_cast操作符通常用于将一种类型转换为另一种不同的类型。对应C语言的强制类型转换。例如:
int main()
{int a = 0;int* ptr = reinterpret_cast<int*>(a); return 0;
}
2.2.3、const_cast
const_cast最常用的用途就是删除变量的const属性,方便赋值。对应与C语言的强制类型转换,例如:
int main()
{volatile const int a = 2;int* p = const_cast<int*>(&a);*p = 3;cout << a << endl; // 上面不加volatile最终会导致这里的a的值的打印为2。cout << *p << endl;cout << (void*)&a << endl; // 之所以进行了转换,是因为cout无法正确识别该类型。cout << p << endl;return 0;
}
2.2.4、dynamic_cast
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:运行时类型识别。C++通过以下方式来支持RTTI:
1、typeid运算符。
2、dynamic_cast运算符。
3、decltype。
上面的这三个在之前的文章中都讲过,不再举例说明。