C++学习记录——삼십일 特殊类设计和类型转换


1、特殊类设计

1、无法拷贝的类

设置成私有;只声明不定义;默认成员函数后加上delete,表示删除此函数,也就不会被拷贝了。

2、只能在堆上创建对象的类

我们可以把析构函数放在私有,但因为每个对象都被要求显式调用析构函数,所以只能new一个对象,但是释放时又不能delete,所以只能在类内写一个销毁用的函数。

class HeapOnly
{
public:
	void Destroy()
	{
		delete this;
	}
private:
	~HeapOnly()
	{
		cout << "~HeapOnly" << endl;
	}
	int _x;
};

int main()
{
	HeapOnly* pho = new HeapOnly;
	pho->Destroy();
	return 0;
}

另一个思路是构造函数放在私有里,这样即使是new也不行,只能在类内写一个初始化用的函数,在类内new一个。

class HeapOnly
{
public:
	HeapOnly* Create(int x = 0)
	{
		HeapOnly* p = new HeapOnly(x);
		return p;
	}
private:
	HeapOnly(int x = 0)
		:_x(x)
	{}
	int _x;
};

int main()
{
	Create(10);
	return 0;
}

但是这样会出错,调不动这个Create函数,因为没法实例化对象,也就不能调用类内成员函数。我们可以把Create函数变成static修饰的来解决,但这个方法还是有坑,因为我们可以拷贝到一个新对象里,这个新对象就会创建在栈里,可以加delete来禁止。

class HeapOnly
{
public:
	static HeapOnly* Create(int x = 0)
	{
		HeapOnly* p = new HeapOnly(x);
		return p;
	}
private:
	HeapOnly(int x = 0)
		:_x(x)
	{}

	HeapOnly(const HeapOnly& hp) = delete;
	HeapOnly& operator=(const HeapOnly& hp) = delete;
	int _x;
};

int main()
{
	HeapOnly* p1 = HeapOnly::Create(10);
	//HeapOnly p2(*p1);
	return 0;
}

3、只能在栈上创建对象的类

把构造函数封掉,那么正常的创建对象方式都没用了,只能借助成员函数来创建,这时候就可以自主选在堆还是栈创建。

class StackOnly
{
public:
	static StackOnly Create(int x = 1)
	{
		return StackOnly(x);
	}
private:
	StackOnly(int x = 0)
		:_x(x)
	{}

	int _x;
};

int main()
{
	StackOnly so1 = StackOnly::Create(10);
	return 0;
}

虽然这样可以在栈上创建,但是也无法阻止在堆上创建,比如这样写:static StackOnly so2 = so1,但是我们也不能封拷贝构造,把拷贝构造放在私有里,因为这样Create函数没法传回来对象了。那既然拷贝构造被禁掉了,就加个移动构造,在公有里加个这样的函数

	StackOnly(StackOnly&& st)
		:_x(st._x)
	{

	}

但是这样也不行,还可以用move的左值来构造。

这方面确实没办法彻底封死。做到这里也就行了。

4、不能被继承的类

构造函数被封掉,也就是给私有化了,也就行了,因为派生类必须调用基类的构造。另外一个办法就是在类后加上final,比如class A final。

5、单例模式

设计模式是代码设计的一些总结,通过长期的实践经验而总结出来的代码格式。适配器,迭代器都是设计模式,单例模式也是设计模式,另外还有工厂、观察者模式。

单例模式是设计一个在一个进程中只能创建一个对象的类。在多线程中使用得多。像内存池,每个线程都去访问它,线程池只需要一份即可,内存池就可以设计成单例模式。

单例模式有两类饿汉模式,懒汉模式。先看饿汉模式

1、饿汉模式

class Singleton
{
public:
	static Singleton* GetInstance()
	{

	}
private:
	Singleton()//限制类外面随意创建对象
	{}
private:
	int _n = 0;
	vector<string> _v;
};

get函数里要如何限制住只能访问一个对象?我们可以在私有里声明一个静态区的变量,在类外面再去定义它。

 class Singleton
{
public:
	static Singleton* GetInstance()
	{
		return _ins;
	}
private:
	Singleton()//限制类外面随意创建对象
	{}
private:
	int _n = 0;
	vector<string> _v;
	static Singleton* _ins;
};

Singleton* Singleton::_ins = new Singleton;

int main()
{
	Singleton::GetInstance();
	return 0;
}

虽然_ins是静态区的,但也是类内的,可以new去初始化它,这里是一个声明定义分离。

在类外面,只能调用get函数来创建这个类的对象,但还不能直接去操作_v和_ins,我们也没有可使用的对象,所以一切操作就得在类内的public内实现。

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		return _ins;
	}

	void Add(const string& str)
	{
		_v.push_back(str);
		++_n;
	}

	void Print()
	{
		for (auto& e : _v)
		{
			cout << e << " ";
		}
		cout << endl;
	}
private:
	Singleton()//限制类外面随意创建对象
	{}
private:
	int _n = 0;
	vector<string> _v;
	static Singleton* _ins;
};

Singleton* Singleton::_ins = new Singleton;

int main()
{
	Singleton::GetInstance()->Add("a");
	Singleton::GetInstance()->Add("b");
	Singleton::GetInstance()->Add("c");
	Singleton::GetInstance()->Print();
	return 0;
}

加入多线程场景和锁。

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <time.h>
using namespace std;

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		return _ins;
	}

	void Add(const string& str)
	{
		_mtx.lock();
		_v.push_back(str);
		++_n;
		_mtx.unlock();
	}

	void Print()
	{
		_mtx.lock();
		for (auto& e : _v)
		{
			cout << e << endl;
		}
		_mtx.unlock();
	}
private:
	Singleton()//限制类外面随意创建对象
	{}
private:
	mutex _mtx;
	int _n = 0;
	vector<string> _v;
	static Singleton* _ins;
};

Singleton* Singleton::_ins = new Singleton;

int main()
{
	//Singleton::GetInstance()->Add("a");
	//Singleton::GetInstance()->Add("b");
	//Singleton::GetInstance()->Add("c");
	//Singleton::GetInstance()->Print();
	int n = 10;
	//lambda体内不能直接用局部变量,需要放到捕获列表中
	thread t1([&]() {
		for (size_t i = 0; i < n; ++i)
		{
			Singleton::GetInstance()->Add("t1线程: " + to_string(rand() + i));
		}
	});
	thread t2([&]() {
		for (size_t i = 0; i < n; ++i)
		{
			Singleton::GetInstance()->Add("t2线程: " + to_string(rand()));
		}
	});
	t1.join();
	t2.join();
	Singleton::GetInstance()->Print();
	return 0;
}

这是一个饿汉模式,在main函数之前就创建对象了,new那一句就是在创建对象,这样对应着饿这个字的意思。

2、懒汉模式

顾名思义,它是需要时才创建对象。

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		if (_ins == nullptr)
		{
			_ins = new Singleton;
		}
		return _ins;
	}
//......
Singleton* Singleton::_ins = nullptr;

3、饿汉懒汉优缺点

饿汉在你没使用前就创建了,不管用户需不需要。饿汉的问题在于,如果单例对象很大,那么一开始申请就会占用资源,也会降低程序启动速度。

如果两个单例都是饿汉模式,并且有依赖关系,要求单例2先创建,再创建单例1,那么饿汉就无法控制顺序。

饿汉的优点就是简单。

懒汉的缺点在于创建对象时会有线程安全问题,如果t1在new时,还没new完就被切走,t2过来new,然后插入数据,t1回来后会再次new一遍,那么t2之前的数据就被覆盖了,没了,所以需要加锁。由于懒汉在new之前还没有创建对象,所以得在priavte里创建一个静态区的锁,然后在外面给初始化。

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		_imtx.lock();
		if (_ins == nullptr)
		{
			_ins = new Singleton;
		}
		return _ins;
		_imtx.unlock();
	}

	void Add(const string& str)
	{
		_vmtx.lock();
		_v.push_back(str);
		++_n;
		_vmtx.unlock();
	}

	void Print()
	{
		_vmtx.lock();
		for (auto& e : _v)
		{
			cout << e << endl;
		}
		_vmtx.unlock();
	}
private:
	Singleton()//限制类外面随意创建对象
	{}
private:
	mutex _vmtx;
	int _n = 0;
	vector<string> _v;
	static Singleton* _ins;
	static mutex _imtx;
};

Singleton* Singleton::_ins = nullptr;
mutex Singleton::_imtx;//初始化了锁

现在我们写的懒汉模式还有问题吗?每一次获取对象都要加锁解锁,但我们只有第一次加锁解锁,那么把锁放在if代码块里可行吗?也不行,线程不安全。只要是只需要第一次加锁的都可以这样写

	static Singleton* GetInstance()
	{
		//双检查加锁
		if (_ins == nullptr)
		{
			_imtx.lock();
			if (_ins == nullptr)//保证线程安全和new一次
			{
				_ins = new Singleton;
			}
			_imtx.unlock();
		}
		return _ins;
	}

懒汉模式解决了饿汉的问题,不过相对复杂点,实际生活中大都使用懒汉。

另外一种懒汉设计

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		static Singleton inst;
		return &inst;
	}

	void Add(const string& str)
	{
		_vmtx.lock();
		_v.push_back(str);
		++_n;
		_vmtx.unlock();
	}

	void Print()
	{
		_vmtx.lock();
		for (auto& e : _v)
		{
			cout << e << endl;
		}
		_vmtx.unlock();
	}

	~Singleton()
	{
	    ;
	}

	//防拷贝
	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton& s) = delete;
private:
	Singleton()
	{
		cout << "Singleton" << endl;
	}
	mutex _vmtx;
	int _n = 0;
	vector<string> _v;
};

局部的静态对象,出了作用域就会自动销毁,也不需要内部类。C++11之后这种方式就可以保证线程安全,C++11中局部静态变量是线程安全的,多线程来调用,只会让一个线程进去函数。

4、对象释放

单例对象不释放,因为全局都要使用它,只有确定不使用它了就可以释放它,这个接口也得在类内定义。

	static void DelInstance()
	{
		_imtx.lock();
		if (_ins)
		{
			delete _ins;
			_ins = nullptr;
		}
		_imtx.unlock();
	}

释放时也有可能特殊要求,析构时要求持久化,也就是里面的数据存到另一个地方去。但是需要每次都调用DelInstance函数,就自动调用析构,完成要求,但如果忘了呢?我们可以定义一个内部类,然后定义一个全局或者静态的内部类对象。

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		//双检查加锁
		if (_ins == nullptr)
		{
			_imtx.lock();
			if (_ins == nullptr)
			{
				_ins = new Singleton;
			}
			_imtx.unlock();
		}
		return _ins;
	}

	void Add(const string& str)
	{
		_vmtx.lock();
		_v.push_back(str);
		++_n;
		_vmtx.unlock();
	}

	void Print()
	{
		_vmtx.lock();
		for (auto& e : _v)
		{
			cout << e << endl;
		}
		_vmtx.unlock();
	}

	static void DelInstance()
	{
		_imtx.lock();
		if (_ins)
		{
			delete _ins;
			_ins = nullptr;
		}
		_imtx.unlock();
	}

	//单例对象回收
	class GC
	{
	public:
		~GC()
		{
			DelInstance();
		}
	};

	static GC _gc;//定义一个全局或者静态的内部类对象
private:
	Singleton()//限制类外面随意创建对象
	{}
private:
	mutex _vmtx;
	int _n = 0;
	vector<string> _v;
	static Singleton* _ins;
	static mutex _imtx;
};

Singleton* Singleton::_ins = nullptr;
mutex Singleton::_imtx;//初始化了锁
Singleton::GC Singleton::_gc;//创建了内部类对象,出了作用域就会自己调用析构

如果忘记了调用释放函数,那么它就可以自动析构,如果没忘记,也没关系,内部类对象析构时会再次调用,然后发现_ins会空,就会什么都不做,解锁。

5、拷贝构造

单例模式还要禁止拷贝构造。有锁的话没法拷贝构造,因为锁不允许拷贝。假设没有锁,我们就要防拷贝。饿汉懒汉都要做。

	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton& s) = delete;

特殊类设计代码

2、类型转换

1、C和C++的类型转换对比

void Test()
{
	int i = 1;
	//隐式类型转换
	double d = i;
	printf("%d %.2f\n", i, d);
	int* p = &i;
	//显式类型转换
	int pa = (int)p;
	printf("%x, %d\n", p, pa);
}

int main()
{
	Test();
	return 0;
}

C语言中相关的类型才能进行转换,但会出现隐式类型提升这个坑点。

C++要兼容C语言,还要解决C语言这方面隐式会导致精度丢失,显式会使代码不够清晰的问题,引入了强制类型转换,static_cast,reinterpret_cast,const_cast,dynamic_cast,四者各有用处,且不能互相套着用。

2、static_cast

用于非多态类型的转换(静态转换),其实就是以前的隐式类型转换,和隐式一样,不能用于两个不相关的类型进行转换,但解决了精度问题。

	double d = 12.34;
	int a = static_cast<int>(d);
	cout << a << endl;

3、reinterpret_cast

适用于不相关类型的转换,意思是重新解释。

	double d = 12.34;
	int a = static_cast<int>(d);
	cout << a << endl;
	int* p = reinterpret_cast<int*>(a);

4、const_cast

用于去掉const属性。

	const int a = 2;
	int* p = const_cast<int*>(&a);
	*p = 3;
	cout << a << endl;
	cout << *p << endl;

实际上这个代码没有改变a的值,但是*p是3。a确实被修改了,但因为const会被编译器优化,默认识别成不能修改的,比如有的编译器会把代码出现a的地方直接替换成2,所以打印出来还是2,也有可能是把a的值放进寄存器中,每次访问都是访问寄存器,所以改了也没用,要解决这个问题,就是加volatile,禁止编译器优化,每次都去内存中访问a的值。

如果我们强制类型转换,int* p = (int*)&a,其实也可以。只是为了统一规范,C++就用const_cast来做。

5、dynamic_cast

C++独有的转换,动态转换,用于基类对象的指针/引用转换为派生类对象的指针或引用。派生类转基类语法本身就支持,而动态转换是解决基类转派生类的。但是无论如何,类对象之间是不能转换的。

class A
{
public:
	virtual void f(){};
};
 
class B : public A
{};

void fun(A* pa)
{
	B* pb1 = static_cast<B*>(pa);//(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;
}

父类指针如果指向子类对象,那么它转子类指针是安全的,用static_cast都可,但如果本身指向父类对象,那么转子类指针就不安全,因为访问子类成员时可能会越界。动态转换如果检测指向父类对象就会转换失败,如果指向子类对象就会转换成功。

可以这样写来查看结果

class A
{
public:
	virtual void f(){};
};

class B : public A
{};

void fun(A* pa, const string& s)
{
	cout << "pa指向" << s << endl;
	B* pb1 = (B*)pa;
	B* pb2 = dynamic_cast<B*>(pa);

	cout << "[强制转换]pb1:" << pb1 << endl;
	cout << "[dynamic_cast转换]pb2:" << pb2 << endl;
}

int main()
{
	A a;
	B b;
	fun(&a, "父类对象");
	fun(&b, "子类对象");
	return 0;
}

6、RTTI

运行时类型识别。RAII是利用对象来管理资源。RTTI通过typeid,dynamic_cast,decltype来支持。

//用上一段代码
int main()
{
	A a;
	B b;
	fun(&a, "父类对象");
	fun(&b, "子类对象");

	cout << typeid(a).name() << endl;//对象类型的字符串
	decltype(a) aa;//获取类型当作aa的类型,用来获取一些不好直接写出来的类型
    function<bool(int,int)> gt = [](int x, int y) {return x > y; };
	set<int, decltype(gt)> s;
	return 0;
}

类型转换代码

结束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值