C++ 学习笔记精要(二)

第一节 特殊类的设计

1. 一个类: 只能在堆上创建对象 

关键点:自己控制析构

1.1 方法一: 使用delete禁掉默认析构函数 

#include <iostream>
using namespace std;


class HeapOnly
{
public:
	HeapOnly()
	{
		_str = new char[10];
	}

	~HeapOnly() = delete;

	void Destroy()
	{
		delete[] _str;

		operator delete(this);
	}

private:
	char* _str;
	//...
};

int main()
{
	HeapOnly* ptr = new HeapOnly;
	ptr->Destroy();
	return 0;
}
  • 只要在堆上申请空间,并且使用delete析构函数禁掉就行了 
  • 自己再实现一个释放空间的函数

1.2 方法二: 将析构函数私有化 

#include <iostream>
#include <stdlib.h>
using namespace std;
class HeapOnly
{
public:
	/*static void Delete(HeapOnly* p)
	{
		delete p;
	}*/
	void Delete()
	{
		delete this;
	}

private:
	// 析构函数私有
	~HeapOnly()
	{
		cout << "~HeapOnly()" << endl;
	}
private:
	int _a;
};

int main()
{
	//HeapOnly hp1;// error
	//static HeapOnly hp2;// error

	HeapOnly* ptr = new HeapOnly;
	ptr->Delete();
	return 0;
}

  1.3 方法三: 将构造函数私有化(禁掉拷贝)

#include <iostream>
#include <stdlib.h>
using namespace std;
class HeapOnly
{
public:
	// 提供一个公有的,获取对象的方式,对象控制是new出来的
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}

	// 防拷贝
	HeapOnly(const HeapOnly& hp) = delete;
	HeapOnly& operator=(const HeapOnly& hp) = delete;
private:
	// 构造函数私有
	HeapOnly()
		:_a(0)
	{}
private:
	int _a;
};

int main()
{
	/*HeapOnly hp1;
	static HeapOnly hp2;

	HeapOnly* hp3 = new HeapOnly;
	delete hp3;*/

	HeapOnly* hp3 = HeapOnly::CreateObj();
	//HeapOnly copy(*hp3);

	delete hp3;

	return 0;
}

直接将构造函数私有化,然后再实现一个CreatObj创建对象,返回值是static;

创建的是堆的话,需要禁掉那2个函数


2. 一个类: 只能在栈上创建对象

关键点: 自己控制构造 

2.1 方法一: 构造函数私有化(禁掉new) 

#include <iostream>
#include <stdlib.h>
using namespace std;
class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		StackOnly st;
		return st;
	}

	// 不能防拷贝
	//StackOnly(const StackOnly& st) = delete;
	//StackOnly& operator=(const StackOnly& st) = delete;
	void* operator new(size_t n) = delete;
private:
	// 构造函数私有
	StackOnly()
		:_a(0)
	{}
private:
	int _a;
};

int main()
{
	/*StackOnly st1;
	static StackOnly st2;
	StackOnly* st3 = new StackOnly;*/

	StackOnly st1 = StackOnly::CreateObj();

	// 拷贝构造
	static StackOnly copy2(st1); // 不好处理,算是一个小缺陷
	//StackOnly* copy3 = new StackOnly(st1);

	return 0;
}

 3. 一个类:不能被继承

3.1 给父类加final关键字 

#include <iostream>
#include <string>
using namespace std;

//C98
//class A
//{
//private:
//	A()
//	{}
//
//protected:
//	int _a;
//};

// C++11中引用的final
class A final
{
public:
	A()
	{}

protected:
	int _a;
};

class B : public A
{

};

int main()
{
	B bb;// 这里对象实例化才会报错

	return 0;
}
  •  C++98中:a. 父类构造函数私有-- 子类是不可见,b. 这种只有对象实例化才会报错

  • C++11中:给父类加上了final关键字,使子类不能继承父类,

4. 一个类: 只能创建一个对象(单例模式)

4.1 单例模式(饿汉模式 && 懒汉模式)

那两种模式都是将构造函数私有化,自己实现一个构造生成一个静态对象

  • 一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享

4.2 饿汉模式: 程序启动时就创建一个唯一的实例对象

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		return &m_instance;
	}
private:
	// 构造函数私有
	Singleton() {};

	// C++11 : 防拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;

	static Singleton m_instance;// 声明
};

Singleton Singleton::m_instance;// 定义
  • 优点:简单

  • 缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。

  • 总结: 如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避 免资源竞争,提高响应速度更好。

4.3 懒汉模式 : 第一次使用对象再创建实例对象

  • 如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取 文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,
  • 就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
#include <iostream>
#include <stdlib.h>
using namespace std;
class MemoryPool
{
public:
	static MemoryPool* GetInstance()
	{
		if (_pinst == nullptr) {
			_pinst = new MemoryPool;
		}

		return _pinst;
	}

	void* Alloc(size_t n)
	{
		void* ptr = nullptr;
		// ....
		return ptr;
	}

	void Dealloc(void* ptr)
	{
		// ...
	}

	// 实现一个内嵌垃圾回收类    
	class CGarbo {
	public:
		~CGarbo()
		{
			if (_pinst)
				delete _pinst;
		}
	};

private:
	// 构造函数私有化
	MemoryPool()
	{
		// ....
	}

	char* _ptr = nullptr;
	// ...

	static MemoryPool* _pinst; // 声明
};

// 定义
MemoryPool* MemoryPool::_pinst = nullptr;

// 回收对象,main函数结束后,他会调用析构函数,就会释放单例对象
static MemoryPool::CGarbo gc;

int main()
{
	void* ptr1 = MemoryPool::GetInstance()->Alloc(10);
	MemoryPool::GetInstance()->Dealloc(ptr1);
}
  • 优点: 有控制顺序, 不影响启动速度
  • 缺点: 相对复杂, 存在线程安全问题

4.4 单例对象释放问题:

  1. 一般情况下,单例对象不需要释放的。因为一般整个程序运行期间都可能会用它。单例对象在进程正常结束后,也会资源释放。
  2. 有些特殊场景需要释放,比如单例对象析构时,要进行一些持久化(往文件、数据库写)操作。 

第二节 C++的类型转换&&IO流

 1. C语言的类型转换

#include <iostream>
#include <string>
using namespace std;

void Insert(size_t pos, char ch)
{
	size_t _size = 5;
	//....
	int end = _size - 1;
	// size_t end = _size - 1;
	while (end >= pos) // end隐式类型转换
	{
		//_str[end + 1] = _str[end];
		--end;
	}
}

void Test1()
{
	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);

	Insert(3, 'a');
	Insert(0, 'a');// 触发死循环
}

int main()
{
	Test1();

	return 0;
}
  • 隐式类型转换(意义相近的类型)
  • 显示的强制类型转换(意义不相近的类型,值转换后有意义)

2. C语言类型转换的缺陷

  1.  隐式类型转化有些情况下可能会出现问题: 比如数据精度丢失
  2. 显示类型转换将所有情况混合在一起,代码不够清晰

3. C++强制类型转换

标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符

3.1 static_cast关键字 -> 隐式类型转换

#include <iostream>
#include <string>
using namespace std;

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

	int* p = &a;
	
	//int address = static_cast<int>(p);// 不支持的
	return 0;
}
  • static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用static_cast就像C语言的隐式类型转换,常用于意义相近的类型
  • 但是它不能用于两个不相关的类型进行转换

 3.2 reinterpret_cast关键字 -> 强制类型转换

#include <iostream>
#include <string>
using namespace std;

int main()
{
	int a = 100;
	int* p = &a;
	int address = reinterpret_cast<int>(p);

	return 0;
}
  • reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型,

  • reinterpret_cast就像C语言的强制类型转换

  • 常用于两个类型不相关的

 3.3 const_cast关键字->取消变量的const属性

  •  const_cast最常用的用途就是删除变量的const属性,方便赋值
  • volatile关键字取消编译器的优化

3.4 dynamic_cast->父类指针 转换 子类指针

dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)  

向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)->切片

向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)

案例一

#include <iostream>
using namespace std;

class A
{
public:
	virtual void f(){}
public:
	int _a = 0;
};

class B : public A
{
public:
	int _b = 1;
};

// A*指针pa有可能指向父类,有可能指向子类
void fun(A* pa)
{
	// 如果pa是指向子类,那么可以转换,转换表达式返回正确的地址
	// 如果pa是指向父类,那么不能转换,转换表达式返回nullptr
	B* pb = dynamic_cast<B*>(pa); // 安全的
	//B* pb = (B*)pa;             // 不安全
	if (pb)
	{
		cout << "转换成功" << endl;
		pb->_a++;
		pb->_b++;
		cout << pb->_a << ":" << pb->_b << endl;
	}
	else
	{
		cout << "转换失败" << endl;
	}
}

int main()
{
	A aa;
		// 父类对象无论如何都是不允许转换成子类对象的
		//B bb = dynamic_cast<B>(aa);// error
		//B bb = (B)aa;// error
	B bb;

	fun(&aa);
	fun(&bb);
	fun(nullptr);

	return 0;
}
  • dynamic_cast只能用于父类含有虚函数的类

  • dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0

案例二

#include <iostream>
using namespace std;
class A1
{
public:
	virtual void f(){}
public:
	int _a1 = 0;
};

class A2
{
public:
	virtual void f(){}
public:
	int _a2 = 0;
};

class B : public A1, public A2
{
public:
	int _b = 1;
};

int main()
{
	B bb;
	A1* ptr1 = &bb;
	A2* ptr2 = &bb;
	cout << ptr1 << endl;
	cout << ptr2 << endl << endl;

	B* pb1 = (B*)ptr1;
	B* pb2 = (B*)ptr2;
	cout << pb1 << endl;
	cout << pb2 << endl << endl;

	B* pb3 = dynamic_cast<B*>(ptr1);
	B* pb4 = dynamic_cast<B*>(ptr2);
	cout << pb3 << endl;
	cout << pb4 << endl << endl;

	return 0;
}

 


3.5 类型转换的实质 

类型转换是通过临时对象来实现的,且临时对象具有常性,

  • 但是尽量不要使用强制类型转换

3.6 常见面试题

  1. C++中的4种类型转换分别是:____ 、____ 、____ 、____。
    1. 分别是static_castreinterpret_castconst_castdynamic_cast
  2. 说说4种类型转换的应用场景。
    1. static_cast用于相近类型的类型之间的转换,编译器隐式执行的任何类型转换都可用
    2. reinterpret_cast用于两个不相关类型之间的转换。
    3. const_cast用于删除变量的const属性,方便赋值。
    4. dynamic_cast用于安全的将父类的指针(或引用)转换成子类的指针(或引用)

4. RTTI->运行时类型识别

RTTI:Run-time Type identifification的简称,即:运行时类型识别 

C++通过以下方式来支持RTTI:

  1. typeid运算符(获取对象类型字符串)
  2. dynamic_cast运算符(父类的指针指向父类对象或者子类对象)
  3. decltype(推导一个对象类型,这个类型可以用来定义另一个对象)

5. C语言的输入与输出

printf/scanf

fprintf/fscanf

sprintf/sscanf

#include <iostream>
using namespace std;
class A
{
public:
    // explicit A(int a)
	A(int a)
		:_a(a)
	{}

	operator int()
	{
		return _a;
	}

private:
	int _a;
};

int main()
{
	// 内置类型 转换成自定义类型
	A aa1 = 1; // 隐式类型转换 用1构造A临时对象,再拷贝构造aa1,优化后直接1构造aa1

	// 自定义类型 转换成内置类型
	int i = aa1;

	return 0;
}

说明一下: 

  •  int i = aa1;能将自定义类型转换成内置类型,主要因为operator int()
  • explicit关键字: 不允许隐式类型的转换

5.1 多组输入

#include <iostream>
using namespace std;
class Date
{
	friend ostream& operator << (ostream& out, const Date& d);
	friend istream& operator >> (istream& in, Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	operator bool()
	{
		// 这里是随意写的,假设输入_year为0,则结束
		if (_year == 0)
			return false;
		else
			return true;
	}
private:
	int _year;
	int _month;
	int _day;
};

istream& operator >> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

ostream& operator << (ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day;
	return out;
}

// C++ IO流,使用面向对象+运算符重载的方式
// 能更好的兼容自定义类型,流插入和流提取
int main()
{
	// 自动识别类型的本质--函数重载
	// 内置类型可以直接使用--因为库里面ostream类型已经实现了
	int i = 1;
	double j = 2.2;
	cout << i << endl;
	cout << j << endl;

	// 自定义类型则需要我们自己重载<< 和 >>
	Date d(2022, 4, 10);
	cout << d;
	while (d)
	{
		cin >> d;
		cout << d;
	}

	return 0;
}

 

  •  while(cin >> d){}遇到文件退出符才结束,因为库里面实现了operator bool()

5.2 fstream文件流 

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
	ifstream ifs("Test.cpp");
	char ch = ifs.get();
	while (ifs)
	{
		cout << ch;
		ch = ifs.get();
	}

	return 0;
}

 

  •  C++中也有对文件进行操作的流fstream
  • 它的使用就可以不用打开文件和关闭文件
  • 库里面写的是一个类它会自己调构造,调析构

5.3 C++ 文件操作 

#include <iostream>
#include <fstream>
using namespace std;
class Date
{
	friend ostream& operator << (ostream& out, const Date& d);
	friend istream& operator >> (istream& in, Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	operator bool()
	{
		// 这里是随意写的,假设输入_year为0,则结束
		if (_year == 0)
			return false;
		else
			return true;
	}
private:
	int _year;
	int _month;
	int _day;
};

istream& operator >> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

ostream& operator << (ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day;
	return out;
}

int main()
{
	ifstream ifs("test.txt");// 默认以读的方式打开
	//fscanf("%d%s%f", )
	int i;
	string s;
	double d;
	Date de;
	ifs >> i >> s >> d >> de;
	cout << i << s << d << de;
	return 0;
}


5.4 二进制文件的读写 && 文本文件的读写

#include <iostream>
#include <fstream>
using namespace std;
struct ServerInfo
{
	char _address[32];
	//string _address;
	int _port;  // 100

	// Date _date;
};

struct ConfigManager
{
public:
	ConfigManager(const char* filename = "server.config")
		:_filename(filename)
	{}

	// 二进制文件的写
	void WriteBin(const ServerInfo& info)
	{
		ofstream ofs(_filename, ios_base::out | ios_base::binary);
		ofs.write((char*)&info, sizeof(info));
	}

	// 二进制文件的写
	void ReadBin(ServerInfo& info)
	{
		ifstream ifs(_filename, ios_base::in | ios_base::binary);
		ifs.read((char*)&info, sizeof(info));
	}

	// 文本文件的写
	void WriteText(const ServerInfo& info)
	{
		ofstream ofs(_filename, ios_base::out);
		ofs << info._address << info._port;
	}

	// 文本文件的读
	void ReadText(ServerInfo& info)
	{
		ifstream ifs(_filename, ios_base::in | ios_base::binary);
		ifs >> info._address >> info._port;
	}

private:
	string _filename; // 配置文件
};
int main()
{
	// 二进制的写
	ServerInfo winfo = { "127.0.0.1", 888 };
	ConfigManager cm;
	cm.WriteBin(winfo);
	// cm.WriteText(winfo);// 文本的写

	// 二进制的读
	ServerInfo rinfo;
	ConfigManager rm;
	rm.ReadBin(rinfo);
	// rm.ReadText(rinfo);// 文本的读
	cout << rinfo._address << endl;
	cout << rinfo._port << endl;
	
	return 0;
}

  • 二进制读写:在内存如何存储,就如何写到磁盘文件 

    • 优点:快

    • 缺点:写出去内容看不见

  • 文本读写:对象数据序列化字符串写出来,读回来也是字符串,反序列化转成对象数据 
    • 优点:可以看见写出去是什么

    • 缺点:存在一个转换过程,要慢一些

 5.5 字符串流-- stringstream

#include <iostream>
#include <fstream>
#include<sstream>

using namespace std;
struct ChatInfo
{
	string _name; // 名字
	int _id;      // id
	string _msg;  // 聊天信息
};

int main()
{
	// 序列化
	ChatInfo winfo = { "张三", 135246, "晚上一起看电影吧" };
	//ostringstream oss;
	stringstream oss;
	oss << winfo._name << endl;
	oss << winfo._id << endl;
	oss << winfo._msg << endl;

	string str = oss.str();
	cout << str << endl;

	// 网络传输str,另一端接收到了字符串串信息数据

	// 反序列化
	ChatInfo rInfo;
	//istringstream iss(str);
	stringstream iss(str);
	iss >> rInfo._name;
	iss >> rInfo._id;
	iss >> rInfo._msg;

	cout << "----------------------------------" << endl;
	cout << rInfo._name << "[" << rInfo._id << "]:>" << rInfo._msg << endl;
	cout << "----------------------------------" << endl;

	return 0;
}

 

第三节 多线程

1.线程库

1.1 thread类的简单介绍

C++11中引入了线程的支持了,使得C++并行编程时不需要依赖第三方库

而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件

函数名

功能

thread()

构造一个线程对象,没有关联任何线程函数,即没有启动任何线程

thread(fn, args1, args2, ...)

构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的

参数

get_id()

获取线程id

jionable()

线程是否还在执行,joinable代表的是一个正在执行中的线程。

jion()

该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行

detach()

在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离

的线程变为后台线程,创建的线程的"死活"就与主线程无关

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的

    状态

  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程

1.2 线程对象关联线程函数

#include <iostream>
using namespace std;
#include <thread>
void ThreadFunc(int a)
{
	cout << "Thread1" << a << endl;
}
class TF
{
public:
	void operator()()
	{
		cout << "Thread3" << endl;
	}
};
int main()
{
	// 线程函数为函数指针
	thread t1(ThreadFunc, 10);

	// 线程函数为lambda表达式
	thread t2([](){cout << "Thread2" << endl; });

	// 线程函数为函数对象
	TF tf;
	thread t3(tf);

	t1.join();
	t2.join();
	t3.join();
	cout << "Main thread!" << endl;
	return 0;
}
  • 线程对象可以关联1.函数指针2.lambda表达式3.函数对象
  • 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程

注意

  1. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造移动赋值,即将一个

    线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。

  2. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
    1. 采用无参构造函数构造的线程对象
    2. 线程对象的状态已经转移给其他线程对象
    3. 线程已经调用jion或者detach结束

1.3 线程函数参数

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

void Print(int n, int& x,mutex& mtx)
{
	for (int i = 0; i < n; ++i)
	{
		mtx.lock();
		cout <<this_thread::get_id()<<":"<< i << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
		++x;
		mtx.unlock();
	}

}

int main()
{
	mutex m;
	int count = 0;
	thread t1(Print, 10, ref(count),ref(m));
	thread t2(Print, 10, ref(count),ref(m);

	t1.join();
	t2.join();

	cout << count << endl;

	return 0;
}

  •  线程函数的参数先传递给thread的,并以值拷贝的方式拷贝到线程栈空间中的
  • 如果不给线程函数的参数不借助ref函数

    • 即使线程参数为引用类型,在线程中修改后也不能修改外部实参

    • 因为其实际引用的是线程栈中的拷贝,而不是外部实参


#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

int main()
{
	mutex mtx;
	int x = 0;
	int n = 10;
	int m;
	cin >> m;

	vector<thread> v(m);
	//v.resize(m);

	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				mtx.lock();

				cout << this_thread::get_id() << ":" << i << endl;
				std::this_thread::sleep_for(std::chrono::milliseconds(100));
				++x;

				mtx.unlock();
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}
  •  借助lambda表达式中的引用捕捉也可以实现上面那个函数,就可以不用借助ref函数

线程并行 && 并发的讨论 

  • 并行:任务的同时进行
  • 并发: 任务的调动和切换
  • 在这个函数中其实是并行的速度更快,因为线程切换十分耗时间

 join与detach

join方式 

主线程创建新线程后,可以调用join函数等待新线程终止,当新线程终止时join函数就会自动清理线程相关的资源

join函数清理线程的相关资源后,thread对象与已销毁的线程就没有关系了,因此一个线程对象一般只会使用一次join,否则程序会崩溃。比如:

void func(int n)
{
	for (int i = 0; i <= n; i++)
	{
		cout << i << endl;
	}
}
int main()
{
	thread t(func, 20);
	t.join();
	t.join(); //程序崩溃
	return 0;
}

但如果一个线程对象join后,又调用移动赋值函数,将一个右值线程对象的关联线程的状态转移过来了,那么这个线程对象又可以调用一次join。比如: 

void func(int n)
{
	for (int i = 0; i <= n; i++)
	{
		cout << i << endl;
	}
}
int main()
{
	thread t(func, 20);
	t.join();

	t = thread(func, 30);
	t.join();
	return 0;
}

但采用join的方式结束线程,在某些场景下也可能会出现问题。比如在该线程被join之前,如果中途因为某些原因导致程序不再执行后续代码,这时这个线程将不会被join 

void func(int n)
{
	for (int i = 0; i <= n; i++)
	{
		cout << i << endl;
	}
}
bool DoSomething()
{
	return false;
}
int main()
{
	thread t(func, 20);

	//...
	if (!DoSomething())
		return -1;
	//...

	t.join(); //不会被执行
	return 0;
}

 因此采用join方式结束线程时,join的调用位置非常关键,为了避免上述问题,可以采用RAII的方式对线程对象进行封装,也就是利用对象的生命周期来控制线程资源的释放。比如:

class myThread
{
public:
	myThread(thread& t)
		:_t(t)
	{}
	~myThread()
	{
		if (_t.joinable())
			_t.join();
	}
	//防拷贝
	myThread(myThread const&) = delete;
	myThread& operator=(const myThread&) = delete;
private:
	thread& _t;
};
  •  每当创建一个线程对象后,就用myThread类对其进行封装产生一个myThread对象
  • 当myThread对象生命周期结束时就会调用析构函数,在析构中会通过joinable判断这个线程是否需要被join,如果需要那么就会调用join对其该线程进行等待。

例如刚才的代码中,使用myThread类对线程对象进行封装后,就能保证线程一定会被join

int main()
{
	thread t(func, 20);
	myThread mt(t); //使用myThread对线程对象进行封装

	//...
	if (!DoSomething())
		return -1;
	//...

	t.join();
	return 0;
}

detach方式 

主线程创建新线程后,也可以调用detach函数将新线程与主线程进行分离,分离后新线程会在后台运行,其所有权和控制权将会交给C++运行库,此时C++运行库会保证当线程退出时,其相关资源能够被正确回收。

  • 使用detach的方式回收线程的资源,一般在线程对象创建好之后就立即调用detach函数
  • 否则线程对象可能会因为某些原因,在后续调用detach函数分离线程之前被销毁掉,这时就会导致程序崩溃
  • 因为当线程对象被销毁时会调用thread的析构函数,而在thread的析构函数中会通过joinable判断这个线程是否需要被join,如果需要那么就会调用terminate终止当前程序(程序崩溃)

1.4 原子性操作库(atomic) 

多线程最主要的问题是共享数据带来的问题(即线程安全)

当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

int main()
{
	mutex mtx;
	atomic<int> x = 0;
	// int x = 0;
	int n = 1000000;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				// t1 t2 t3 t4
				++x;
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}

 

  •  C++98中传统的解决方式:可以对共享修改的数据加锁保护
    • 加锁的问题: 这个线程执行的时候, 其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁
  • C++11中使用atomic类模板,定义出需要的任意原子类型

    • 程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。

注意 

#include <atomic>
int main()
{
     atomic<int> a1(0);
     //atomic<int> a2(a1);   // 编译失败
     atomic<int> a2(0);
     //a2 = a1;               // 编译失败
     return 0;
}
  • 原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,
  • 因此在C++11 中,原子类型只能从其模板参数中进行构造不允许原子类型进行拷贝构造、移动构造以及 operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了 

1.5 lock_guardunique_lock  

多线程环境下,原子性只能保证某个变量的安全性

多线程环境下,而需要保证一段代码的安全性,就只能通过加锁的方式实现

lock_guard

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

//RAII
template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lk)
		:_lock(lk)
	{
		_lock.lock();
		cout << "thread:" << this_thread::get_id() << "加锁" << endl;
	}

	~LockGuard()
	{
		cout << "thread:" << this_thread::get_id() << "解锁" << endl << endl;
		_lock.unlock();
	}
private:
	Lock& _lock;// 成员变量是引用
};

int main()
{
	mutex mtx;
	atomic<int> x = 0;
	//int x = 0;
	int n = 100;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				{
					lock_guard<mutex> lk(mtx);
					cout << this_thread::get_id() << ":" << i << endl;
				}

				std::this_thread::sleep_for(std::chrono::milliseconds(100));
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}
  •  lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封
  • 调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

  • lock_guard缺陷太单一,用户没有办法对该锁进行控制

unique_lock 

与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数

  • 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
  • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有 权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
  • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相 同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

1.6 条件变量库(condition_variable)

condition_variable中提供的成员函数,可分为wait系列和notify系列两类。

wait系列成员函数

wait系列成员函数的作用就是让调用线程进行阻塞等待,包括waitwait_forwait_until

下面先以wait为例进行介绍,wait函数提供了两个不同版本的接口:

//版本一
void wait(unique_lock<mutex>& lck);
//版本二
template<class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);
  • 调用第一个版本的wait函数时只需要传入一个互斥锁,线程调用wait后会立即被阻塞,直到被唤醒。
  • 调用第二个版本的wait函数时除了需要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。

注意 调用wait系列函数时,传入互斥锁的类型必须是unique_lock。 

notify系列成员函数 

notify系列成员函数的作用就是唤醒等待的线程,包括notify_onenotify_all 

  • notify_one:唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。
  •  notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做

注意 条件变量下可能会有多个线程在进行阻塞等待,这些线程会被放到一个等待队列中进行排队 

1.7 实现两个线程交替打印1-100 

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;

	thread t1([&](){
		while (i < n)
		{
			mtx.lock();

			cout << this_thread::get_id() << ":" << i << endl;
			i += 1;

			mtx.unlock();
		}
	});

	this_thread::sleep_for(chrono::microseconds(100));

	thread t2([&](){
		while (i < n)
		{
			mtx.lock();

			cout << this_thread::get_id() << ":" << i << endl;
			i += 1;

			mtx.unlock();
		}
	});

	t1.join();
	t2.join();

	return 0;
}

  • 在线程切换的中间时间也会发现线程竞争抢锁的问题 

正确解决方案(条件变量) 

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;
	condition_variable cv;// 条件变量
	bool ready = true;

	// t1打印奇数
	thread t1([&](){
		while (i < n)
		{
			{
				unique_lock<mutex> lock(mtx);
				cv.wait(lock, [&ready](){return !ready; });// 等待线程

				cout << "t1--" << this_thread::get_id() << ":" << i << endl;
				i += 1;

				ready = true;

				cv.notify_one();// 解除线程等待
			}

			//this_thread::yield();
			this_thread::sleep_for(chrono::microseconds(100));
		}
	});

	// t2打印偶数
	thread t2([&]() {
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&ready](){return ready; });

			cout <<"t2--"<<this_thread::get_id() << ":" << i << endl;
			i += 1;
			ready = false;

			cv.notify_one();
		}
	});

	this_thread::sleep_for(chrono::seconds(3));

	cout << "t1:" << t1.get_id() << endl;
	cout << "t2:" << t2.get_id() << endl;

	t1.join();
	t2.join();

	return 0;
}

 

  • cv.wait(lock, [&ready]() {return !ready; });
    • ready返回的是false时,这个线程就会阻塞
    • 阻塞当前线程,并自动调用lock.unlock()允许其他锁定的线程继续执行
  •  cv.notify_one();
    • 唤醒当前线程并自动调用lock.lock();就只允许自己一个线程执行

1.8 shared_ptr的多线程问题

#include<iostream>
#include<thread>
#include<mutex>
#include<vector>
#include<atomic>
#include<memory>
using namespace std;

namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pRefCount(new int(1))
			, _pMutex(new mutex)
		{}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pRefCount(sp._pRefCount)
			, _pMutex(sp._pMutex)
		{
			AddRef();
		}

		void Release()
		{
			bool flag = false;

			_pMutex->lock();
			if (--(*_pRefCount) == 0 && _ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pRefCount;

				flag = true;
			}
			_pMutex->unlock();

			if (flag)
				delete _pMutex;
		}

		void AddRef()
		{
			_pMutex->lock();

			++(*_pRefCount);

			_pMutex->unlock();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();

				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
				_pMutex = sp._pMutex;
				AddRef();
			}

			return *this;
		}

		int use_count()
		{
			return *_pRefCount;
		}

		~shared_ptr()
		{
			Release();
		}

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pRefCount;// 使用时需要加锁
		mutex* _pMutex;// 锁指针
	};
}

int main()
{
	// shared_ptr是线程安全的吗?
	bit::shared_ptr<double> sp1(new double(1.11));
	bit::shared_ptr<double> sp2(sp1);

	mutex mtx;

	vector<thread> v(2);
	int n = 100000;
	for (auto& t : v)
	{
		t = thread([&](){
			for (size_t i = 0; i < n; ++i)
			{
				// 拷贝是线程安全的
				bit::shared_ptr<double> sp(sp1);

				// 访问资源不是
				mtx.lock();
				(*sp)++;
				mtx.unlock();
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << sp1.use_count() << endl;
	cout << *sp1 << endl;

	return 0;
}
  •  在多线程中,shared_ptr也应该对自己的引用计数进行加锁处理

  • 在多线程中, shared_ptr的拷贝是线程安全的,但访问资源不是,所以访问资源也需要加锁

1.9 单例模式的多线程问题 

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 保护第一次,后续不需要加锁
		// 双检查加锁
		if (_pInstance == nullptr)
		{
			unique_lock<mutex> lock(_mtx);
			if (_pInstance == nullptr)
			{
				_pInstance = new Singleton;
			}
		}

		return _pInstance;
	}

private:
	// 构造函数私有
	Singleton(){};

	// C++11
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;

	static Singleton* _pInstance;
	static mutex _mtx;
};

Singleton* Singleton::_pInstance = nullptr;
mutex Singleton::_mtx; 

int main()
{
	Singleton::GetInstance();
	Singleton::GetInstance();

	return 0;
}
  •  在多线程的情况下, 第一次创建对象时也是需要加锁保护

巧妙的解决方案

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		static Singleton _s;// 局部的静态对象,第一次调用时初始化

		return &_s;
	}

private:
	// 构造函数私有
	Singleton() {};

	// C++11
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
};

int main()
{
	Singleton::GetInstance();
	Singleton::GetInstance();

	return 0;
}
  • 局部的静态对象,第一次调用时初始化
  • 在C++11之前是不能保证线程安全的
    静态对象的构造函数调用初始化并不能保证线程安全的原子性
  • C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上玩

 第四节 日期类

1. 日期类的实现

class Date
{
public:
	// 构造函数
	Date(int year = 0, int month = 1, int day = 1);
	// 打印函数
	void Print() const;
	// 日期+=天数
	Date& operator+=(int day);
	// 日期+天数
	Date operator+(int day) const;
	// 日期-=天数
	Date& operator-=(int day);
	// 日期-天数
	Date operator-(int day) const;
	// 前置++
	Date& operator++();
	// 后置++
	Date operator++(int);
	// 前置--
	Date& operator--();
	// 后置--
	Date operator--(int);
	// 日期的大小关系比较
	bool operator>(const Date& d) const;
	bool operator>=(const Date& d) const;
	bool operator<(const Date& d) const;
	bool operator<=(const Date& d) const;
	bool operator==(const Date& d) const;
	bool operator!=(const Date& d) const;
	// 日期-日期
	int operator-(const Date& d) const;

	// 析构,拷贝构造,赋值重载可以不写,使用默认生成的即可

private:
	int _year;
	int _month;
	int _day;
};

1.1 构造函数

// 获取某年某月的天数
inline int GetMonthDay(int year, int month)
{
	// 数组存储平年每个月的天数
	static int dayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	int day = dayArray[month];
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
	{
		//闰年2月的天数
		day = 29;
	}
	return day;
}
// 构造函数
Date::Date(int year, int month, int day)
{
	// 检查日期的合法性
	if (year >= 0
	&& month >= 1 && month <= 12
	&& day >= 1 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		// 严格来说抛异常更好
		cout << "非法日期" << endl;
		cout << year << "年" << month << "月" << day << "日" << endl;
	}
}
  •  GetMonthDay函数会被多次调用,所以最好设置成内联函数
  • 且该函数中的月天数用static修饰,避免每次调用该函数都需要重新开辟数组。
  • 当函数声明和定义分开时,在声明时注明缺省参数定义时不标出缺省参数

1.2 打印函数

// 打印函数
void Date::Print() const
{
	cout << _year << "年" << _month << "月" << _day << "日" << endl;
}

1.3 日期 += 天数

// 日期+=天数
Date& Date::operator+=(int day)
{
	if (day<0)
	{
		// 复用operator-=
		*this -= -day;
	}
	else
	{
		_day += day;
		// 日期不合法,通过不断调整,直到最后日期合法为止
		while (_day > GetMonthDay(_year, _month))
		{
			_day -= GetMonthDay(_year, _month);
			_month++;
			if (_month > 12)
			{
				_year++;
				_month = 1;
			}
		}
	}
	return *this;
}
  1. 首先判断日期是否合法 

  2. 若日已满,则日减去当前月的天数,月加一

  3. 若月已满,则将年加一,月置为1

1.4 日期 + 天数

// 日期+天数
Date Date::operator+(int day) const
{
	Date tmp(*this);// 拷贝构造tmp,用于返回
	// 复用operator+=
	tmp += day;

	return tmp;
}
  •  复用代码

1.5 日期 -= 天数

// 日期-=天数
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		// 复用operator+=
		*this += -day;
	}
	else
	{
		_day -= day;
		// 日期不合法,通过不断调整,直到最后日期合法为止
		while (_day <= 0)
		{
			_month--;
			if (_month == 0)
			{
				_year--;
				_month = 12;
			}
			_day += GetMonthDay(_year, _month);
		}
	}
	return *this;
}
  • 首先判断日期是否合法 

  • 若日为负数,则月减一

  • 若月为0,则年减一,月置为12
  • 日加上当前月的天数

1.6 日期 - 天数 

// 日期-天数
Date Date::operator-(int day) const
{
	Date tmp(*this);// 拷贝构造tmp,用于返回
	// 复用operator-=
	tmp -= day;

	return tmp;
}
  •   复用代码

1.8 自增自减运算符的实现

前置++

// 前置++
Date& Date::operator++()
{
	// 复用operator+=
	*this += 1;
	return *this;
}
  •  复用代码 

后置 ++

// 后置++
Date Date::operator++(int)
{
	Date tmp(*this);// 拷贝构造tmp,用于返回
	// 复用operator+=
	*this += 1;
	return tmp;
}
  • 注意: 后置++需要多给一个参数 
  •  复用代码

前置 - -

// 前置--
Date& Date::operator--()
{
	// 复用operator-=
	*this -= 1;
	return *this;
}
  •  复用代码

后置 - - 

// 后置--
Date Date::operator--(int)
{
	Date tmp(*this);// 拷贝构造tmp,用于返回
	// 复用operator-=
	*this -= 1;
	return tmp;
}
  • 注意: 后置++需要多给一个参数 
  •  复用代码

1.9 比较运算符的实现

> 运算符的重载

bool Date::operator>(const Date& d) const
{
	if (_year > d._year)
	{
		return true;
	}
	else if (_year == d._year)
	{
		if (_month > d._month)
		{
			return true;
		}
		else if (_month == d._month)
		{
			if (_day > d._day)
			{
				return true;
			}
		}
	}
	return false;
}

== 运算符的重载

bool Date::operator==(const Date& d) const
{
	return _year == d._year
		&&_month == d._month
		&&_day == d._day;
}

 >= 运算符的重载

bool Date::operator>=(const Date& d) const
{
	return *this > d || *this == d;
}

< 运算符的重载

bool Date::operator<(const Date& d) const
{
	return !(*this >= d);
}

<= 运算符的重载

bool Date::operator<=(const Date& d) const
{
	return !(*this > d);
}

 != 运算符的重载

bool Date::operator!=(const Date& d) const
{
	return !(*this == d);
}

 1.10 日期 - 日期

// 日期-日期
int Date::operator-(const Date& d) const
{
	Date max = *this;// 假设第一个日期较大
	Date min = d;// 假设第二个日期较小
	int flag = 1;// 此时结果应该为正值
	if (*this < d)
	{
		// 假设错误,更正
		max = d;
		min = *this;
		flag = -1;// 此时结果应该为负值
	}
	int n = 0;// 记录所加的总天数
	while (min != max)
	{
		min++;// 较小的日期++
		n++;// 总天数++
	}
	return n*flag;
}

说明一下: 

  • 较小的日期的天数一直加一,直到最后和较大的日期相等即可
  • 代码中使用flag变量标记返回值的正负
    flag为1代表返回的是正值,flag为-1代表返回的是值,
    最后返回总天数与flag相乘之后的值即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值