目录
[2.1 方法一(使用指针)](#2.1 方法一(使用指针))
[2.2 方法二(使用静态成员)](#2.2 方法二(使用静态成员))
[六、 RTTI](#六、 RTTI)
前言:
不知不觉,已经学习了很多新知识了,但是学海无涯,我们已经掌握了大部分C++11的新语法,本篇我们要学习在一些特定的情况下设计出特殊需求的类。还有之前没有讲解过的类型转换等知识,做好准备。go go go出发了!
一、设计只能在堆上创建对象的类
1.方法一(私有化构造函数)
我们平时这样写,可以在任意内存空间上创建对象:
cpp
class HeapOnly
{
};
int main()
{
HeapOnly hp1; //栈上开辟
HeapOnly* hp2 = new HeapOnly; //堆上开辟
return 0;
}
我们先把构造函数封死,写成私有的:
cpp
class HeapOnly
{
private:
HeapOnly()
{}
};
但是这样也无法在堆上创建对象了,我们开放一个方法使其可以构造对象。
cpp
public:
HeapOnly* CreateObj()
{
return new HeapOnly;
}
但是调用这个函数要有对象,这就矛盾了。是否还记得static方法无需通过对象调用?所以使其变成静态的成员方法,加上static。
类中static修饰的成员或方法 ,无需实例化对象,只需要指定类域即可使用。
对于静态成员,需要在类外单独定义(分配存储空间)。访问权限仍然受类域限制。
静态成员变量一般存储在全局/静态存储区。
静态成员函数一般存储在代码区。
cpp
class HeapOnly
{
public:
static HeapOnly* CreateObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
};
int main()
{
//HeapOnly hp1; //栈上开辟
//HeapOnly* hp2 = new HeapOnly; //堆上开辟
HeapOnly* hp3 = HeapOnly::CreateObj();
return 0;
}
但是这样就无法在栈上创建对象了吗?我们可以通过拷贝构造创建对象。
cpp
HeapOnly hp4(*hp3); //创建对象依旧在栈上
所以我们平时会把拷贝构造和赋值重载都给禁用掉:
cpp
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;
所以我们平时会把拷贝构造和赋值重载都给禁用掉。析构函数直接delete即可。
2.方法二(私有化析构函数)
还有第二种方式,把析构写成私有的。

但是我们使用delete也无法释放hp2,因为delete由析构函数和operator delete构成。

可以像之前一样,提供一个公共的方法专门释放空间。

这里在里面也就调用了析构函数。
二、设计只能在栈上创建对象的类
我们还是像之前一样私有化构造方法:

此时我们还是通过提供的静态方法来创建对象:
cpp
StackOnly s4 = StackOnly::CreateObj();
还有问题,我们可以调用拷贝构造在堆上创建对象:

我们不能把拷贝构造封死,因为Create返回的是StackOnly,会调用拷贝构造,这样甚至无法创建对象了。

C++规定,如果一个类重载了operator new,必须使用重载的new。
补充一个知识:new和operator new的关系。
new:
完整的内存分配 + 对象构造
new 是一个高级运算符,完成两步操作:
分配内存:调用底层 operator new 分配原始内存。
构造对象:在分配的内存上调用构造函数初始化对象。
operator new:
仅负责内存分配
是 new 的第一步操作,只分配指定大小的原始内存(不涉及对象构造)。
比如这条语句:
cpp
StackOnly* s5 = new StackOnly(s4);
这条语句中,执行顺序是严格分两步的,具体流程如下:先分配内存(调用 operator new)。 再调用拷贝构造函数(在分配的内存上构造对象)。
所以为了防止这样的情况发生,我们把operator new和operator delete都禁止掉。
cpp
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
我们使用拷贝构造直接在栈上创建对象没有问题,但是还可以在静态区继续创建对象:
cpp
StackOnly s6(s4); //在栈上创建对象
static StackOnly s7(s6); //在静态区创建对象
我们可以把拷贝构造禁用掉,之后提供移动构造。
cpp
//移动构造
StackOnly(StackOnly&& s)
{}
//禁用拷贝构造
StackOnly(const StackOnly& s) = delete;
但是还是可以在静态区上创建对象,比如:
cpp
static StackOnly s6(move(s4));
所以很难十全十美。
三、单例模式
设计一个类,只能创建一个对象。
比如服务器信息,有IP、端口号等等,我们可以把它设计成一个类,但是只能有一个实例对象。
1.饿汉模式
饿汉模式(Eager Initialization) 是一种单例模式的实现方式,其核心特点是在程序启动时(早于 main 函数执行)就提前初始化单例对象,无论后续是否真正用到该实例。它的设计思想是"提前准备好资源",因此得名"饿汉"。
有些抽象,它的意思是在类中定义了一个该对象。
C++类的大小不包括静态成员变量的大小。静态成员属于类本身,不是类实例。
cpp
class InfoMgr
{
public:
static InfoMgr& GetInstance()
{
return _ins;
}
void Print()
{
cout << _ip << endl;
cout << _port << endl;
cout << _buffSize << endl;
}
private:
InfoMgr()
{}
private:
string _ip = "127.0.0.1";
int _port = 80;
size_t _buffSize = 1024 * 1024;
//静态对象
static InfoMgr _ins;
};
//类外面定义
InfoMgr InfoMgr::_ins;
在类中声明了一个静态成员变量 _ins,并在类外实例化了它。

InfoMgr 类通过静态成员 _ins 确保整个程序只有唯一一个实例对象(单例模式)。这个实例是全局唯一的,因为 _ins 是静态的,且构造函数是私有的(防止外部创建新对象)。
因为没有禁用拷贝构造,这样会发生浅拷贝,并在栈上创建一个InfoMgr对象。所以我们把拷贝构造和赋值重载都禁用掉:
cpp
InfoMgr(const InfoMgr&) = delete;
InfoMgr& operator=(const InfoMgr&) = delete;
但是饿汉模式有缺陷,当有多个饿汉模式的单例,某个对象初始化内容较多(读文件),导致程序启动慢。
饿汉模式(Eager Initialization)的单例实现之所以可能导致程序启动变慢,是因为它在程序初始化阶段(早于 main 函数执行)就完成了单例对象的构造。如果单例的构造函数涉及复杂操作(如加载资源、建立连接、初始化大量数据等),会直接拖慢启动时间。
A和B两个饿汉,对象初始化存在依赖关系,要求A先初始化,B再初始化,饿汉无法保证。
比如在一个文件中定义了一个饿汉,另一个文件中也定义了饿汉,无法保证谁先初始化。
2.懒汉模式
2.1 方法一(使用指针)
我们将其对象定义为指针,并初始化为空,当第一次使用静态方法创建对象时,判断为空,为空就new一个对象出来,这样能保证在调用时创建对象且是唯一一个。
cpp
//懒汉模式
class InfoMgr
{
public:
static InfoMgr& GetInstance()
{
if (_pins == nullptr)
{
_pins = new InfoMgr;
}
return *_pins;
}
void Print()
{
cout << _ip << endl;
cout << _port << endl;
cout << _buffSize << endl;
}
private:
InfoMgr(const InfoMgr&) = delete;
InfoMgr& operator=(const InfoMgr&) = delete;
InfoMgr()
{}
private:
string _ip = "127.0.0.1";
int _port = 80;
size_t _buffSize = 1024 * 1024;
//静态对象
static InfoMgr* _pins;
};
//类外面定义
InfoMgr* InfoMgr::_pins = nullptr;

单例对象一般也不需要释放,因为只有一个,当主函数调用完毕释放。 但是我们来实现一个释放空间的函数:
cpp
static void DelInstance()
{
delete _pins;
_pins = nullptr;
}
2.2 方法二(使用静态成员)
我们可以不使用指针,在静态函数内部调用时实例化一个对象出来:


支持C++11和之后版本。
四、C++的类型转换
1.内置类型之间的转换
内置类型之间的转换分为两种:
- 隐式类型的转换
- 显示类型的转换
整形系列,整形和浮点数直接。显示类型是一些类型有一些关联,但是不是非常紧密,需要强制转换,比如指针和整形,不同类型指针之间:
cpp
int main()
{
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);
return 0;
}

2.内置类型转换为自定义类型
其实我们之前就已经见到过这种方法了,它是支持的。它们是通过自定义类型构造函数决定的,会走隐式类型转换:
cpp
class A
{
public:
A(int a)
: _a1(a)
, _a2(a)
{}
A(int a1, int a2)
: _a1(a1)
, _a2(a2)
{}
private:
int _a1 = 1;
int _a2 = 1;
};
int main()
{
string s1 = "1111";
A aa1 = 1; //单参数隐式类型转换
A aa2 = { 2, 2 }; //多参数隐式类型转换
return 0;
}
如果不想支持隐式类型转换,可以在构造函数前加上explicit声明。

3.自定义类型转换为内置类型
自定义类型能否转换为内置类型?这里我们需要去重载,其实可以重载(),但是它被仿函数占用了,于是有了新语法:
cpp
//()被仿函数占用了
// operator + 类型 无返回值
operator int()
{
return _a1 + _a2;
}

智能指针的operator bool就使用了这个东西,所以可以做判断,具体使用方法看文档。
4.自定义类型直接的转换
再写一个B类,如果A类需要转换成B,需要在B类中添加构造方法:
cpp
class B
{
public:
B(int b)
: _b1(b)
{}
//使A转换成B
B(const A& aa)
: _b1(aa.get())
{}
private:
int _b1 = 1;
};
调用A类中的get,所以在A类中添加get方法:
cppint get() const { return _a1 + _a2; }

所以自定义类型之间可以互相转换,使用对应的构造函数支持。
我们再举一个例子,我们之前实现过list,使用const迭代器对非const对象进行遍历就会报错。

但是,据我们所学,这里是权限的缩小,应该可以遍历。我们先看标准库是否可以这样遍历。

可以发现没有问题。
权限的缩小和放大,仅限于指针和引用。我们这里是自定义类型,必须在内部处理才能实现。
所以这里是类型转换,而不是权限缩小。
同一个类模板给不同的参数,会实例化不同的类型,所以iterator和const_iterator是两个完全不同的类型。

所以为了实现这个类型转换,我们刚才说自定义类型之间的转换是要通过构造函数的,所以写一个拷贝构造函数,用于 允许 ListIterator<T, T&, T*>(普通迭代器)向 ListIterator<T, const T&, const T*>(const 迭代器)的隐式转换。
cpp
ListIterator(const ListIterator<T, T&, T*>& it)
: _head(it._head)
{}

注意这里的const迭代器是关注我们在list中声明的const_iterator中const修饰的哪一部分,我们这里修饰的是指向内容,所以可以被++。
五、C++强制类型转换
标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
- static_cast
- reinterpret_cast
- const_cast
- dynamic_cast
1.static_cast
static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用 static_cast,但它不能用于两个不相关的类型进行转换。

2.reinterpret_cast
我们之前说过,对于关联性强的可以直接隐式类型转换,而关联性不强的需要用到强制转换,比如执行转指针,所以要用到reinterpret_cast(reinterpret重释):

3.const_cast
对于const类型,我们需要对指针加上const修饰才能取其地址,但是const_cast可以把这个限制去掉:

这里是因为编译器的优化,所以我们加上关键字volatile(易变的;挥发的;无定性的):

4.dynamic_cast
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换) 向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则) 向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的) 注意: 1. dynamic_cast只能用于父类含有虚函数的类。
我们一般就是向上转型,也就是子转父 ,这样比较安全;但是可能有特殊需求需要父转子 ,我们可以强制转换 ,但是这种方式是不安全 的,所以要使用dynamic_cast来转换。
cpp
class A
{
public:
virtual void f() {}
};
class B : public A
{};
void fun(A* pa)
{
// dynamic_cast会先检查是否能转换成功,能成功则转换
// 不能则返回nullptr
//B* pb1 = static_cast<B*>(pa);
B* pb2 = dynamic_cast<B*>(pa);
//cout << "pb1:" << pb1 << endl;
if (pb2)
{
cout << "pb2:" << pb2 << endl;
}
else
{
cout << "转换失败" << endl;
}
}
int main()
{
A a;
B b;
fun(&a); //父类指针转换为子类指针失败
fun(&b); //子类指针转换为子类指针成功
return 0;
}

需要父类中有虚函数,没有虚函数报错。
六、 RTTI
RTTI :Run-time Type identification的简称,即:运行时类型识别。
C++通过以下方式来支持RTTI:
- typeid运算符
- dynamic_cast运算符
- decltype
总结:
有一些知识点可能过得比较快,但用法也确实简单,这篇我们学习的内容和之前相比很简单,我们学的可能也并不深入,但是至少要有所了解,以后工作中很有可能会使用到,所以需要把这部分学习一遍。加油吧少年!
