线程 & QThread使用 & 信号槽第五参数
1. 事件循环
对于文件发送这类需要花费大量资源的行为,若陷入死循环会占用大量的CPU资源,应当使用sleep(100)主动释放CPU:在一段时间后被添加到就绪队列中,等待切换到执行态(大于100ms)
在QT中,若在某个组件的槽函数(调用事件后的执行函数)中,执行耗时特别久的内容,会影响主事件循环,导致出现未响应。解决方法:使用多线程。
int main(int argc,char* argv[]){ QApplication a(argc,argv);//初始化窗口系统,并使用命令行参数构造main中应用程序对象 Dialog w; //对话窗对象 w.show(); //显示窗口 return a.exec(); //进入主事件循环 } static QApplication::exec(){ while(1){ //事件循环 //执行事件:比如事件响应 槽函数 //return exit(); } } void Dialog::on_pb_test_clicked(){ //模拟发送文件 while(1){ //未响应 qDebug()<<"发文件"; _sleep(100); } }
2. 多线程
如何创建线程
1)CreateThread (WINAPI)ExitThread(会导致内存泄漏)
2)pthread_create(linux)pthread_join()
3)std::thread(c++线程对象)
4)_beginthreadex(C运行时库) _endthreadex(会回收C运行时库使用的内存块)
_beginthreadex(0/*安全级别*/, 0/*线程栈大小,默认为1M*/, &ThreadFunc/*线程函数*/, (void*)this/*参数,传入当前类*/, 0/*创建即运行*/, 0); //__stdcall 调用约定 unsigned __stdcall ThreadFunc (void *arg){ //模拟发送文件 while(1){ qDebug()<<"发文件"; _sleep(100); } }
Ui::Dialog *ui;是私有的,如何使用:
-
通过公有get方法,获取ui
-
在Dialog类中声明友元
friend unsigned __stdcall ThreadFunc (void *arg); unsigned __stdcall ThreadFunc (void *arg){ //让按钮隐藏 Dialog* p=(Dialog*)arg; p->ui->pb_show->hide(); }
发现,程序崩溃了。原因:在另一个线程中,不允许操作主界面的控件。
解决方法:可以利用信号和槽传回主线程进行操作
信号和槽的条件:
-
Q_OBJECT 宏
-
connect连接信号和槽
-
发送信号和定义槽函数的类,需要是QObject的子类
5)QThread
6)QObject::moveToThread
3. QThread
使用QThread实现,满足信号与槽的条件
实现QThread的子类,可以发信号
class Dialog:public QDialog { Q_OBJECT MyThread* m_thread; }; Dialog::Dialog(){ qDebug()<<"main thread:"<<QThread::currentThreadId(); //获取当前线程HANDLE m_thread=new MyThread; } void Dialog::on_pb_test_clicked(){ m_thread->start(); //->run() } class MyThread:public QThread{ //自定义线程类 Q_OBJECT //定义线程函数 void run(){//重写父类的虚函数 qDebug()<<"this thread:"<<QThread::currentThreadId(); } };
执行start()开启新的线程,新线程调用run()开启线程的执行。run()函数结束,线程退出
如果线程正在运行(run()正在执行),start()调用后什么也不做
//实现用另一个线程隐藏控件 class MyThread : public QThread{ Q_OBJECT signals: void SIG_toHide(); public: void run(){ //隐藏控件 Q_EMIT SIG_toHide(); } }; class Dialog : public QDialog{ Q_OBJECT private slots: void slot_toHide(); }; Dialog::Dialog(){ m_thread=new MyThread; connect(m_thread,SIGNAL(SIG_toHide()),this,SLOT(slot_toHide())); } Dialog::~Dialog(){ if(m_thread->isRunning()){ m_thread->quit(); m_thread->wait(100); if(m_thread->isRunning()){ m_thread->terminate(); m_thread->wait(100); } delete m_thread; } } void Dialog::slot_toHide(){ qDebug()<<__func__<<":"<<QThread::currentThreadId(); ui->pb_show->hide(); }
4. connect
QObject::connect(const QObject *sender, /*信号发出者*/ const char *signal, /*信号*/ const QObject *receiver, /*信号接收者*/ const char *method, /*槽函数*/ Qt::ConnectionType type = Qt::AutoConnection /*连接方法,默认自动连接*/ );
connect(m_thread,SIGNAL(SIG_toHide()),this,SLOT(slot_toHide()), (Qt::ConnectionType)(Qt::QueuedConnection | Qt::UniqueConnection));
DirectConnection:在同一个线程里直接连接,效果等同于直接调用函数
QueuedConnection:不在一个线程里执行,发送信号后马上执行后续代码
BlockingQueuedConnection:不在一个线程里执行,等待槽函数返回后执行后续代码
5. moveToThread
-
QObject子类定义
-
创建一个QThread对象
-
子类对象调用moveToThread,Qthread对象调用start()开启线程
-
定义信号槽,接收到信号,就让子类的槽函数执行(在移动到的目标线程执行)
class WorkObject : public QObject{ Q_OBJECT public slots: void slot_thisWork(){ qDebug()<<__func__<<":"<<QThread::currentThreadId(); } }; class Dialog : public QDialog{ Q_OBJECT signals: void SIG_toDoWork(); private: WorkObject* m_worker; QThread* m_workerThread; }; Dialog::Dialog(){ m_worker=new WorkObject; m_workerThread=new QThread; m_worker->moveToThread(m_workerThread); connect(this,SIGNAL(SIG_toDoWork()),m_worker,SLOT(slot_thisWork())); m_workerThread->start(); } Dialog::~Dialog(){ delete m_worker; if(m_workerThread->isRunning()){ m_workerThread->quit(); m_workerThread->wait(100); if(m_workerThread->isRunning()){ m_workerThread->terminate(); m_workerThread->wait(100); } delete m_workerThread; } } void Dialog::on_pb_todoWork_clicked(){ qDebug()<<__func__<<"main:"<<QThread::currentThreadId(); Q_EMIT SIG_toDoWork(); }
使用moveToThread,不会像QThread一样每次start()创建一个新线程,而是一直一个线程。可以让函数到另一个线程中执行。
class Dialog{ public: static Dialog* m_pThis; }; Dialog* Dialog::m_pThis=nullptr; Dialog::Dialog{ m_pThis=this; //m_workerThread->start(); SendFile(); //主线程 } void WorkObject::slot_thisWork(){ qDebug()<<__func__<<":"<<QThread::currentThreadId(); Dialog::m_pThis->SendFile(); //子线程 }
优点:可以将一段要执行的代码,放到另一个线程执行,同时一直在一个线程里执行。QThread通过run()函数,也可以将一段要执行的代码,放到另一个线程执行,但是每次start()都会创建一个新的线程,相比moveToThread效率低,线程开销大,耗费额外的空间。
C++11线程 & 3种锁
1. C++11线程 std::thread
thread对象要结合join()或detach()使用,避免线程未结束线程对象生命周期就结束了
#include <thread> int add(int a,int b){ //线程函数没有要求格式,比较灵活 qDebug()<<"a+b="<<a+b; _sleep(1000); qDebug()<<"end"; return a+b; } std::thread th(&add,3,4); //线程函数以参数形式传入 _sleep(100); qDebug()<<"before"; th.join(); //th.detach(); qDebug()<<"after";
join()阻塞当前线程,直到线程函数退出
detach()将线程对象与线程函数分离,线程不依赖线程对象管理
join()和detach(),两者必选其一,不然线程对象的回收会影响线程的回收,导致程序崩溃
class AA{ AA(){ //在构造函数中创建线程 //如果使用_beginthreadex,线程函数需是静态的 std::thread th(&threadFun,this); //类成员函数,第一个参数是隐藏的this th.detach(); } void threadFun(){ while(!m_isQuit){ _sleep(100); qDebug()<<"do some work"; } } private: bool m_isQuit=false; };
2. 线程同步
解决线程并发引起的数据问题
并发:同一时间间隔内,程序交替执行
线程同步:同一时间,只允许一个线程可以读写某资源
方式:锁、关键段、读写锁、原子访问、信号、事件
2.1 互斥锁
#include <mutex> std::mutex mtx; mtx.lock(); //访问全局资源 mtx.unlock();
RAII(Resource Access Is Initializer)资源获取即初始化
例如:STL容器对象,开辟的空间在堆区,对象空间看定义的位置
lock_guard、unique_lock 管理互斥锁,遵循RAII,让互斥锁使用更方便更安全(避免死锁)
{ std::lock_guard<std::mutex> lck(mtx); //在初始化lck时,会对互斥锁上锁 }//lck生命周期结束,自动释放锁
unique_lock 可以在中途释放锁。lock_guard可以借助{}实现(lock_guard耗费的资源更少、更快)
std::unique_lock<std::mutex> unlck(mtx); unlck.unlock();
粒度:锁定代码的长度。锁定的代码长度越长,锁的粒度越大,影响并发的效率
2.2 条件变量
#include <condition_variable> std::condition_variable con_var; wait(/*一个已经上锁的互斥锁*/, /*可调用对象(函数指针、仿函数、bind、lamba表达式),若返回值是false,会通知无效仍阻塞在wait(),避免误通知*/); //阻塞当前线程,直到收到通知 //当调用wait()时,释放互斥锁,阻塞当前线程,将线程放入条件变量等待的容器中 //当收到通知时,获取互斥锁,执行后续代码 notify_one; //通知一个 notify_all;//通知所有
std::condition_variable con_var; void ConditionFun(){ std::unique_lock<std::mutex> unlck(mtx); qDebug()<<"before wait"; con_var.wait(unlck); qDebug()<<"after wait"; } int main(){ std::thread th[3]; for(int i=0;i<3;i++){ th[i]=std::thread(&ConditionFun); } for(int i=0;i<3;i++){ th[i].detach(); } } extern std::condition_variable con_var; //链接到外部同名变量 void Dialog::on_pb_notify_clicked(){ //通知条件变量 con_var.notify_one(); }
bool check(){ return false; } con_var.wait(unlck,&check); //返回值是false,通知失效
注意一定是wait()之后才能收到通知,不然通知就失效了
2.3 原子访问
原子访问仅对某个变量修改值时锁定,速度相对其他同步方式更快
#include <atomic> std::atomic<int> count(0); count++; //加锁的方式进行++,注意:++ --操作是原子操作,可以保证线程安全 count--; //枷锁的方式进行-- count.load(); //读值 //count=count+10; //ERROR:非原子操作,线程不安全
std::atomic<int> cnt(0); void AtomicFunc(){ for(int i=0;i<100;i++){ _sleep(10); cnt++; _sleep(10); } } int main(){ std::thread th[3]; for(int i=0;i<3;i++){ th[i]=std::thread(&AtomicFunc); } for(int i=0;i<3;i++){ th[i].join(); } qDebug()<<"count="<<cnt.load(); }
C++11 lambda表达式
在函数里定义另一个函数,例如:sort()
bool comp(int a,int b){ //comp(a,b)为真,保持顺序,从大到小 return a>b; } vector<int> vec={1,3,5,2,4,6}; sort(vec.begin(),vec.end(),&comp); //sort默认为从小到大 //数量较少使用插入排序,大量数据使用快排,同时为了避免出现因为数据集导致快排退化、会使用堆排序辅助排序
可以使用lambda表达式定义一个匿名函数,在函数定义里完成对另一个函数的定义
sort(vec.begin(),vec.end(), [] (int a,int b) {return a>b;}); //作为匿名函数使用 auto 函数名=lambda表达式; //通过函数名,调用函数
-
捕获列表:捕获(lambda表达式)外面的局部的变量,以及this指针。静态和全局的变量不用捕获,直接能用
[ = ] 捕获外面的非静态局部的全部变量的副本,值传递,以及this指针
[ & ] 捕获外面的非静态局部的全部变量的引用,引用传递,以及this指针
[ val ] 捕获val变量,值传递
[ &val ] 捕获val的引用,引用传递
[ this ] 捕获this指针
class AA{ public: AA(){ auto f=[this](){ cout<<"this:"<<this<<endl; }; f(); } }; connect(ui->pushButton,&QPushButton::clicked, this,[this](){ this->ui->pushButton->setText("改变名字");});
-
参数列表:与函数的参数列表是一样,可省。但有mutable修饰时不可省
-
mutable:可变的。没有mutable修饰时是一个特殊的常函数,不可以修改非引用的值(副本不可改)
-
返回值:可省,自动匹配返回值类型
-
函数体:与函数写法一致
C++11 智能指针
C++11现代化C++,让代码更容易简洁
导致内存泄漏的原因:
-
堆区没有手动释放空间
-
父类没有虚析构,回收父类只调用了父类析构
-
文件
-
网络套接字socket
-
资源句柄HANDLE(线程、窗口)
把指针变成对象,在生命周期结束时,通过析构函数自动回收空间
#include <memory> auto_ptr unique_ptr shared_ptr weak_ptr
1. auto_ptr
为什么使用指针?
利用指针实现地址传递,间接访问修改空间值
#include <memory> class AAA{ public: AAA(string _str):m_str(_str){ cout<<"AAA::AAA(str):"<<m_str<<endl; } ~AAA(){ cout<<"AAA::~AAA()"<<endl; } void say(){ cout<<"say:"<<m_str<<endl; } private: string m_str; }; void autoFunc(std::auto_ptr<AAA> apt /* =apt2 */){ //apt2在传参时发生拷贝构造,apt2为空 } int main() { { cout<<"auto ptr"<<endl; //创建 智能指针接收创建的堆空间 std::auto_ptr<AAA> apt1(new AAA("auto_ptr1")); std::auto_ptr<AAA> apt2; apt2.reset(new AAA("auto_ptr2")); //清空已有的创建新的 std::auto_ptr<AAA> apt3=apt1; //拷贝构造,不允许两个指针指向同一空间,原指针赋空 //std::auto_ptr<AAA> apt3(apt1); //使用 apt2.get()->say(); apt2->say(); //判断 if(apt1.get()==nullptr){ //拷贝构造,原指针为空 cout<<"apt1 null point"<<endl; } autoFunc(apt2); //apt2变为空指针 //apt2->say(); //ERROR:apt2为空 //回收 //reset() } return 0; }
C++会避免类型转换,在我们不知情的情况下创建对象,发生拷贝构造,把空间转移了
2. unique_ptr
唯一所有权的智能指针,某一个空间只有一个智能指针持有。不允许系统未经用户允许就发生空间转移,需要手动使用std::move()
//AAA(const AAA &a)=delete; //删除拷贝构造,禁止外部调用C++自动生成的函数 { cout<<"unique ptr"<<endl; //创建 智能指针接收创建的堆空间 std::unique_ptr<AAA> upt1(new AAA("unique_ptr1")); std::unique_ptr<AAA> upt2; upt2.reset(new AAA("unique_ptr2")); //清空已有的创建新的 //std::unique_ptr<AAA> upt3=upt1; //ERROR:使用了一个删除的函数,unique_ptr禁止了拷贝构造和拷贝赋值 std::unique_ptr<AAA> upt3; //upt3=upt1; //ERROR:使用了一个删除的函数,unique_ptr禁止了赋值重载运算符 upt3=std::move(upt1); //使用转移语句实现空间转移,避免重复资源,使用权传递 upt3->say(); if(upt1.get()==nullptr){ cout<<"upt1 null point"<<endl; } }
3. shared_ptr
共享使用权的智能指针,允许多个智能指针指向同一个空间。使用引用计数,实现回收
特性:
-
引用计数:指向相同空间,引用计数+1;智能指针回收,引用计数-1;引用计数归0,空间回收
-
引用计数+1 -1是线程安全的
-
使用use_count()查看引用计数
{ //创建一个shared_ptr对象 shared_ptr<AAA> spt1(new AAA("shared_ptr1")); shared_ptr<AAA> spt2=std::make_shared<AAA>("shared_ptr2"); shared_ptr<AAA> spt3; spt3.reset(new AAA("shared_ptr3")); { shared_ptr<AAA> spt4=spt1; //指向同一个,共享 cout<<"spt1 use_count:"<<spt1.use_count()<<endl; //2 spt4->say(); } spt1->say(); cout<<"spt1 use_count:"<<spt1.use_count()<<endl; //1 }
问题:循环引用
class BBBB; class AAAA{ public: AAAA(){ cout<<"AAAA::AAAA()"<<endl; } ~AAAA(){ cout<<"AAAA::~AAAA()"<<endl; } shared_ptr<BBBB> spb; }; class BBBB{ public: BBBB(){ cout<<"BBBB::BBBB()"<<endl; } ~BBBB(){ cout<<"BBBB::~BBBB()"<<endl; } shared_ptr<AAAA> spa; }; { shared_ptr<AAAA> sa=shared_ptr<AAAA>(new AAAA); shared_ptr<BBBB> sb=shared_ptr<BBBB>(new BBBB); sa->spb=sb; sb->spa=sa; cout<<"A use count:"<<sa.use_count()<<endl; cout<<"B use count:"<<sb.use_count()<<endl; //~BBBB() 生命周期结束,没有调用析构进行回收 //~AAAA() 生命周期结束,回收sb sa,但spb spa与空间共存,一直占用空间,导致引用计数不归零,无法回收空间 } //回收顺序——函数栈FILO
4. weak_ptr
不使用使用权的智能指针,和shared_ptr配合使用,单独不可以指向空间。
weak_ptr访问空间,需要使用lock()函数,获取shared_ptr然后才能获得空间。
指向shared_ptr的空间,空间的引用计数不加1。弱引用,解决了循环引用。
weak_ptr的销毁不会影响引用计数。
expire()方法,可以用于判断引用计数是否为0。若为真,代表引用计数为0,空间回收
class BBBB{ public: BBBB(){ cout<<"BBBB::BBBB()"<<endl; } ~BBBB(){ cout<<"BBBB::~BBBB()"<<endl; } //shared_ptr<AAAA> spa; //将两者中任意一个shared_ptr改为weak_ptr即可解决循环引用 weak_ptr<AAAA> spa; //指向空间,引用计数不加1 }; { shared_ptr<AAAA> sa=shared_ptr<AAAA>(new AAAA); shared_ptr<BBBB> sb=shared_ptr<BBBB>(new BBBB); sa->spb=sb; sb->spa=sa; sb->spa.lock()->spb; cout<<"A use count:"<<sa.use_count()<<endl; //1 cout<<"B use count:"<<sb.use_count()<<endl; //2 { weak_ptr<BBBB> wpb=sb; cout<<"B use count:"<<sb.use_count()<<endl; //2 cout<<wpb.expired()<<endl; //0 } cout<<"B use count:"<<sb.use_count()<<endl; //2 }
A的引用计数为1,生命周期结束,回收sb sa后,A引用计数归零,先回收A空间。spb回收,B引用计数归0,B空间回收。
手写智能指针 实现线程安全的shared_ptr
#include <iostream> #include<atomic> using namespace std; class AAA{ public: AAA(string _str):m_str(_str){ cout<<"AAA::AAA(str):"<<m_str<<endl; } AAA(const AAA &a)=delete; //删除拷贝构造 ~AAA(){ cout<<"AAA::~AAA()"<<endl; } void say(){ cout<<"say:"<<m_str<<endl; } private: string m_str; }; template<class T> struct Node{ //sA1 sA2如何共用ptr和count?指向相同的Node地址 Node(T* _ptr){ //接收外部空间 ptr=_ptr; nCount=1; } ~Node(){ //回收空间 delete ptr; nCount=0; } T* ptr; //int nCount; atomic<int> nCount; //原子访问 }; template<class T> //写typename或class没区别 class MyShared_ptr{ public: //创建,是第一个指向空间的 MyShared_ptr(T* _ptr){ m_ptr_count=new Node<T>(_ptr); } //拷贝构造,已有对象初始化该对象 MyShared_ptr(const MyShared_ptr& _ptr){ m_ptr_count=_ptr.m_ptr_count; m_ptr_count->nCount++; } //回收,触发引用计数-1,归0 回收 ~MyShared_ptr(){ if(m_ptr_count){ m_ptr_count->nCount--; if(m_ptr_count->nCount==0){ delete m_ptr_count; m_ptr_count=nullptr; } } } //赋值重载操作符,返回值为当前对象 const MyShared_ptr& operator =(const MyShared_ptr& _ptr){ if(&_ptr==this) return *this; //原来的空间的引用计数-1,归0回收 if(m_ptr_count){ m_ptr_count->nCount--; if(m_ptr_count->nCount==0){ delete m_ptr_count; } //新空间引用计数+1 m_ptr_count=_ptr.m_ptr_count; m_ptr_count->nCount++; } return *this; //this为当前对象的地址 } //引用计数 指向相同空间是共用的 int use_count(){ if(m_ptr_count) return m_ptr_count->nCount.load(); return 0; } //get方法 T* get(){ if(m_ptr_count) return m_ptr_count; return nullptr; } private: Node<T>* m_ptr_count; //指针成员,采取浅拷贝,为了共用 }; int main(){ { MyShared_ptr<AAA> spa(new AAA("my shared ptr a")); { MyShared_ptr<AAA> spb=(new AAA("my shared ptr b")); spb=spa; cout<<"a use count:"<<spa.use_count()<<endl; } cout<<"a use count:"<<spa.use_count()<<endl; } getchar(); return 0; }
C++11 chrono时间库
1. 精度ratio
#include <chrono> template <intmax_t N, intmax_t D = 1> class ratio; //其中N表示分子,D表示分母,默认用秒表示的时间单位。 typedef ratio<60, 1> minute; //60s typedef ratio<1, 1> second; //1s typedef ratio<1,1000> milli; //1/1000s
2. duration 时间间隔
struct duration{ typedef _Rep rep; //数值 typedef _Period period; //单位 }; duration_cast<转换的目标类型>(原值).count(); //截断转换时间,成员函数count()返回单位时间的数量
用std::chrono::duration 表示一段时间
std::this_thread::sleep_for(std::chrono::milliseconds(2000)); //线程休眠 //this_thread 当前线程 //sleep_for(chrono的时间间隔)休眠一段时间,使用chrono的休眠,增加易读性 //milliseconds 毫秒 2000ms->2s typedef duration<int64_t, milli> milliseconds; std::chrono::duration<2000,ratio<1,1000>>
3. 时间点
Epoch Time:1970-01-01 00:00:00 UTC UNIX系统认为Epoch Time为时间纪元,也就是说在UNIX系统中,一切的时间会以Epoch Time为一个基点,然后通过经过秒数再来计算现如今的时间(UNIX时间戳)。
UTC:世界统一时间,由原子钟提供
GMT:格林威治时间,由本初子午线确定,00:00:00对应北京时间08:00:00
template <class Clock, class Duration = typename Clock::duration> class time_point; duration time_since_epoch(); //用于获取当前时间点距离时间戳的时间长度duration
std::chrono::time_point 表示一个具体时间,第一个模板参数Clock用来指定所要使用的时钟,第二个模板函数参数用来表示时间的计量单位(特化的std::chrono::duration<> )。time_point也就是距离时间戳(epoch)的时间长度(duration)
//时间点 std::chrono::system_clock::time_point tp=std::chrono::system_clock::now(); //获取当前时间点 time_t t_t=std::chrono::system_clock::to_time_t(tp); tm* t=std::localtime(&t_t); //获取毫秒 auto du=tp.time_since_epoch(); //获取unix时间戳 //转换 duration_cast<转换的目标类型>(原值) int ms=chrono::duration_cast<chrono::milliseconds>(du).count(); ms=ms%1000; //打印当前时间 printf("%d-%02d-%02d %02d:%02d:%02d %03d\n", t->tm_year+1900, t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, ms); return 0;
4. 时钟(代表当前系统的时间)
4.1 system_clock系统时钟
获取当前系统时间的时钟,系统时间可以改变。不是连续的,一般不用于计算时间间隔
4.2 steady_clock稳定时钟
不受系统时间影响,单调递增,用于计算时间间隔
4.3 high_resolution_clock高分辨率时钟
system_clock或steady_clock的别名,由系统决定。提供高分辨率的时间计算
//使用稳定时钟计算一段时间间隔 std::chrono::steady_clock::time_point begin=chrono::steady_clock::now(); std::this_thread::sleep_for(chrono::seconds(1)); std::chrono::steady_clock::time_point end=chrono::steady_clock::now(); //两个时间点的差——>时间间隔 auto du=end-begin; //duration类型 //转换为ms单位 int diff=chrono::duration_cast<chrono::milliseconds>(du).count(); cout<<"diff:"<<diff<<endl;
C++11 线程安全的单例模式
#include <mutex> call_once(once_flag& flag,函数, 参数...); //只执行一次的函数 //执行一次后,自动将flag反转,使函数只执行一次
单例模式的优点:
-
保证一个类只有一个对象,避免在不知情的情况下创建对象(类型转换),造成内存泄漏等问题
-
提供全局的调用点,方便编程
-
提供延迟初始化,并且是线程安全的,可以避免在程序刚运行时初始化
为什么需要是单例?
1)避免多个不同引起歧义,相同需要同步。例如:任务管理器,打印机任务队列、线程池、日志、管理者
2)避免在不知情的情况下创建对象,造成内存泄露
实现:
1)将析构、构造、拷贝构造私有化
2)提供静态公有到的获取对象的方法
//简易版本:使用静态存储区,保证线程安全 class Singleton{ private: Singleton()=default; //使用默认的构造 ~Singleton()=default; Singleton(const Singleton& s)=delete; //禁用拷贝构造 Singleton& operator =(const Singleton& s)=delete; public: static Singleton* getInstance(){ static Singleton s; return &s; } }; //问题:静态存储区生命周期为整个程序运行,无法主动回收
1. 饿汉式
//C++11版本 使用堆区 class Singleton{ private: Singleton()=default; //使用默认的构造 ~Singleton()=default; Singleton(const Singleton& s)=delete; //禁用拷贝构造 Singleton& operator =(const Singleton& s)=delete; static Singleton* m_p; //在.cpp文件中赋值,避免重定义 public: static Singleton* getInstance(){ return m_p; } static void destroyInstance(){ //饿汉式,在结束手动调用回收 if(m_p){ delete m_p; m_p=nullptr; } } }; Singleton* Singleton::m_p=new Singleton; //饿汉式,起始就要给对象
2. 懒汉式
//C++11版本 使用堆区 class Singleton{ private: Singleton()=default; //使用默认的构造 ~Singleton()=default; Singleton(const Singleton& s)=delete; //禁用拷贝构造 Singleton& operator =(const Singleton& s)=delete; static Singleton* m_p; //在.cpp文件中赋值,避免重定义 static once_flag flag; static std::mutex m_mutex; public: static Singleton* getInstance(){ //懒汉式,需要时手动创建对象 /* 线程安全:call_once() * if(!m_p){ //m_p=new Singleton; //不是线程安全的 call_once(flag,[](){Singleton::m_p=new Singleton}); }*/ //双层判断 double_check,解决需要等锁造成的效率低下 if(!m_p){ std::lock_guard<std::mutex> lck(m_mutex); if(!m_p){ m_p=new Singleton; } } return m_p; } static void destroyInstance(){ //释放资源 if(m_p){ std::lock_guard<std::mutex> lck(m_mutex); if(m_p){ delete m_p; m_p=nullptr; } } } }; Singleton* Singleton::m_p=nullptr; //懒汉式,啥时候用啥时候给 once_flag Singleton::flag; std::mutex Singleton::m_mutex;
3. 智能指针封装
class Singleton{ private: Singleton()=default; //使用默认的构造 Singleton(const Singleton& s)=delete; //禁用拷贝构造 Singleton& operator =(const Singleton& s)=delete; static std::unique_ptr<Singleton> m_pS; static once_flag flag; static std::mutex m_mutex; public: ~Singleton()=default; static Singleton& getInstance(){ //双层判断 if(!m_pS.get()){ std::lock_guard<std::mutex> lck(m_mutex); if(!m_pS.get()){ m_pS.reset(new Singleton); } } return *(m_pS.get()); } static void destroyInstance(){ if(m_pS.get()!=nullptr){ m_pS.reset(); } } }; std::unique_ptr<Singleton> Singleton::m_pS; once_flag Singleton::flag; std::mutex Singleton::m_mutex;
4. 定义销毁器解决智能指针实现单例析构私有无法访问
#include <iostream> #include <mutex> #include <memory> using namespace std; class Singleton{ private: //私有构造函数,防止外部创建实例 Singleton(){ cout<<"Singleton created"<<endl; } //私有析构函数 ~Singleton(){ cout<<"Singleton destoryed"<<endl; } //解决方法:unique_ptr自定义一个销毁器deleter //定义内容结构:内部类结构体是外部类的友元 struct deleter{ void operator ()(Singleton *p)const{ delete p; } }; static std::unique_ptr<Singleton,deleter> instance; static std::mutex m_mutex; public: static Singleton& getInstance(){ if(!instance.get()){ std::lock_guard<std::mutex> lck(m_mutex); if(!instance.get()){ instance.reset(new Singleton); } } return *instance; } static void destroyInstance(){ instance.reset(); } void doSomething(){ cout<<"Doing something"<<endl; } }; std::unique_ptr<Singleton,Singleton::deleter> Singleton::instance; std::mutex Singleton::m_mutex; int main() { //获取单例实体并调用方法 Singleton& singleton=Singleton::getInstance(); singleton.doSomething(); cout<<"addr:"<<&Singleton::getInstance()<<endl; cout<<"addr:"<<&Singleton::getInstance()<<endl; //delete &Singleton::getInstance(); return 0; }
实现日志
日志:存消息 警告 错误信息(同时记录时间),以备以后出现问题分析问题定位问题
-
创建文件保存(不可以只记录输出结果,而不写入文件中持久化保存)
不把所有日志存在一个文件中,不好查,不稳定,存在隐患
达到设定时间间隔就关闭文件,开启新文件;设置行数,达到预设行数(计数器),切换文件
日志文件命名:year-month-day_hour_min_sec_ms.log,精确到ms,保证文件切换时不会重名
-
文件写入同步
fwrite写入文本之后,把文本写入缓冲区,文件内容同步fflush,才能把文本写到文件中
fflush(int fd);
现代化操作系统,为了避免反复中断(开销大),会有一个缓冲区,在写入或发送数据时 ,先写入缓冲区,等待 缓冲区满 或 超时后,才会把数据同步完成写入或发送。
-
写入日志实现
多线程并发写(锁)会导致乱序。在处理流程里写文件,需要等待文件IO,会导致处理流程延迟增大。
可以使用另一个线程写入文本,不影响处理流程。
使用队列存储,采用特殊的生产者消费者模型——单一消费者
【拓展:异步双缓冲日志】
-
生产者消费者模型
(1)组成部分:若干生产者、若干消费者 /管理者/ 同步队列(阻塞队列)
(2)成员任务:生产者负责生产任务,添加到同步队列中。消费者监听同步队列,有任务从队列中拉取任务。
(3)阻塞:当队列满,生产者阻塞。当队列为空,消费者阻塞。/*管理者负责调配生产者和消费者的个数。*/
队列实现:deque、环形队列
线程控制方法:条件变量和互斥锁
-
日志是一个单例
//CMyLog.h #ifndef CMYLOG_H #define CMYLOG_H #include <iostream> #include <string.h> using namespace std; #include <deque> #include <mutex> #include <condition_variable> #include <chrono> #include <thread> //日志宏的实现 enum ENUM_LOG_TYPE{ info =0, warning, error }; #define WRITE_LOG(a,b) CMyLog::getInstance()->writeLog(a,b); //1.实现单例 //2.实现框架 class CMyLog{ public: static CMyLog* getInstance(){ static CMyLog log; return &log; } //写日志 信息 警告 void writeLog(int type,string content){ //格式化字符串 todo string str=content; { //加锁 std::lock_guard<std::mutex> lck(m_mutex); //添加日志到队列 m_queue.emplace_back(str); //解锁 } //条件变量 通知 m_conVar.notify_one(); } private: CMyLog(){ init(); //创建线程 std::thread th(&CMyLog::writeThread,this); th.detach(); } ~CMyLog(){ //退出标志为真 m_quitFlag=true; //条件变量 通知 m_conVar.notify_one(); std::this_thread::sleep_for(chrono::microseconds(100)); } CMyLog(const CMyLog& log)=delete; CMyLog& operator=(const CMyLog& log)=delete; void init(){ //赋初值 m_file=fopen("./log.log","w"); m_quitFlag=false; //行数计数 todo //文档打开时间 todo } void writeThread(){ while(!m_quitFlag){ //加锁:lockguard RAII 自动解锁 std::unique_lock<std::mutex> ulck(m_mutex); //循环避免错误通知 while(!m_quitFlag && m_queue.empty()){ //条件变量等通知 m_conVar.wait(ulck); } //队列非空 if(!m_queue.empty()){ //队列中取出数据 string res=m_queue.front(); m_queue.pop_front(); //判断是否切换文件 todo //写文件 fwrite(res.c_str(),1,strlen(res.c_str()),m_file); //同步文件 fflush(m_file); } } //关闭文件 fclose(m_file); } private: //日志文件指针 FILE* m_file; //队列 std::deque<string> m_queue; //互斥锁 std::mutex m_mutex; //条件变量 std::condition_variable m_conVar; //线程退出标志 bool m_quitFlag; //行数计数 todo //文档打开时间 todo }; #endif // CMYLOG_H
获取时间
struct MyTime{ int year; int month; int day; int hour; int min; int sec; unsigned int msec; static MyTime getCurTime(){ MyTime curTime; //时间点 std::chrono::system_clock::time_point tp=std::chrono::system_clock::now(); //获取当前时间点 time_t t_t=std::chrono::system_clock::to_time_t(tp); tm* t=std::localtime(&t_t); //获取毫秒 auto du=tp.time_since_epoch(); //获取unix时间戳(返回值是时间间隔duration) int ms=chrono::duration_cast<chrono::milliseconds>(du).count(); ms=ms%1000; //返回当前时间 curTime.year=t->tm_year+1900; curTime.month=t->tm_mon+1; curTime.day=t->tm_mday; curTime.hour=t->tm_hour; curTime.min=t->tm_min; curTime.sec=t->tm_sec; curTime.msec=ms; return curTime; } }; //写日志 信息 警告 void writeLog(int type,string content){ MyTime curTime=MyTime::getCurTime(); char buf[100]=""; sprintf(buf,"[%04d_%02d_%02d %02d:%02d:%02d %03d]:", curTime.year,curTime.month,curTime.day, curTime.hour,curTime.min,curTime.sec, curTime.msec); //格式化字符串 string str; switch (type) { case info: str="info "; break; case warning: str="warning "; break; case error: str="error "; break; default: str="default"; break; } str+=buf; str+=content; { //加锁 std::lock_guard<std::mutex> lck(m_mutex); //添加日志到队列 m_queue.emplace_back(str); //解锁 } //条件变量 通知 m_conVar.notify_one(); /*先解锁再发条件变量通知 *这样writeThread解除阻塞并重新加锁,保证writeLog阻塞住 *这时候通知一定能接收到 */ }
切换文件
#define _MAX_LINE_COUNT 50 void init(){ //文档打开时间 m_openTime=MyTime::getCurTime(); //赋初值 char buf[100]=""; sprintf(buf,"./%04d_%02d_%02d_%02d_%02d_%02d_%03d.log", m_openTime.year,m_openTime.month,m_openTime.day, m_openTime.hour,m_openTime.min,m_openTime.sec, m_openTime.msec); m_file=fopen(buf,"w"); //build路径 m_quitFlag=false; //行数计数 m_count=0; } void writeThread(){ while(!m_quitFlag){ //加锁:lockguard RAII 自动解锁 std::unique_lock<std::mutex> ulck(m_mutex); //循环避免错误通知 while(!m_quitFlag && m_queue.empty()){ //条件变量等通知 m_conVar.wait(ulck); } //队列非空 if(!m_queue.empty()){ //队列中取出数据 string res=m_queue.front(); m_queue.pop_front(); //判断是否切换文件 MyTime curTime=MyTime::getCurTime(); if(m_count>=_MAX_LINE_COUNT || curTime.day>m_openTime.day || curTime.month>m_openTime.month || curTime.year>m_openTime.year){ fclose(m_file); init(); } //写文件 fwrite(res.c_str(),1,strlen(res.c_str()),m_file); //同步文件 fflush(m_file); m_count++; } } //关闭文件 fclose(m_file); }
搜索小例子
1)搜狗接口比较简单,替换query即可实现搜索,"搜狗搜索引擎 - 上网从搜狗开始雾霾&pid=AWNb5-7772"
2)从返回的数据中可以发现,data-url="海纳各领域知识,新一代百科全书 - 搜狗百科....可以得到雾霾词条的百科页面
3)词条的内容就是以 <div id='lemmaPage'> <div id='j-shareAbstract'>开始的一段文本, 直到</div>结束 那么剩下的就是根据正则表达式就可以筛选出最终的结果.
编辑
用途:
-
可以搜索数据
-
获取天气
-
开发新闻控件
-
实现http服务器,客户端通过http请求完成通信
(一)封装NetRequest
#.pro include(./NetRequest/NetRequest.pri) INCLUDEPATH += ./NetRequest/
#.pri QT +=network
//NetRequest.h #ifndef NETREQUEST_H #define NETREQUEST_H #include <QObject> #include<QNetworkAccessManager> #include<QNetworkRequest> #include<QNetworkReply> class NetRequest : public QObject { Q_OBJECT signals: void SIG_dealResponse(int code, QByteArray& bt); //不能多线程处理,使用&避免拷贝 public: explicit NetRequest(QObject *parent = nullptr); ~NetRequest(); void sendGetRequest(QString url); public slots: void slot_replyFinished(QNetworkReply* reply); private: //这里使用的类QNetworkAccessManager 用于管理网络连接, QNetworkRequest 用来实现请求 QNetworkAccessManager *network_manager ; //发送请求 QNetworkRequest *network_request; }; #endif // NETREQUEST_H
//NetRequest.cpp #include "netrequest.h" NetRequest::NetRequest(QObject *parent) : QObject(parent) { //创建类的对象 network_manager = new QNetworkAccessManager(); network_request = new QNetworkRequest(); //写请求后得到回复的处理 connect(network_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(slot_replyFinished(QNetworkReply*)) ); } NetRequest::~NetRequest() { network_manager->deleteLater(); delete network_request; } void NetRequest::sendGetRequest(QString url) { network_request->setUrl(QUrl(url)); // url是Qstring 类型, 表示要请求的url链接 network_manager->get(*network_request); } void NetRequest::slot_replyFinished(QNetworkReply *reply) { //无错误处理 if(reply->error() == QNetworkReply::NoError) { QByteArray bytes = reply->readAll(); //获取字节 //发送信号 处理数据 Q_EMIT SIG_dealResponse(200,bytes); //用200表示成功 }else{ //处理错误 qDebug()<<"处理错误"; //发送信号 QByteArray bt; Q_EMIT SIG_dealResponse(-1, bt ); //用-1表示失败 } reply->deleteLater(); }
(二)发送请求
重点部分:发送请求、获取回复
包含头文件 #include<QNetworkAccessManager> #include<QNetworkRequest> #include<QNetworkReply> 我们需要用到的类为 QNetworkAccessManager *network_manager ; //发送请求搜狗搜索 QNetworkRequest *network_request; //这里使用的类QNetworkAccessManager 用于管理网络连接, QNetworkRequest 用来实现请求 创建类的对象 network_manager = new QNetworkAccessManager(); network_request = new QNetworkRequest(); 写请求后得到回复的处理 connect(network_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(slot_replyFinished(QNetworkReply*)) ); 发送url请求 network_request->setUrl(QUrl(url)); // url是Qstring 类型, 表示要请求的url链接 network_manager->get(*network_request); 通过上面的步骤可以将请求发送, 一旦对方给出请求回复, 那么会发出finish信号, 进而有connect的槽函数 slot_replyFinished(QNetworkReply*)进行处理.
(三)解析,二次请求
void MainWindow::slot_dealResponse(int code, QByteArray &bt) { if(code>0){ qDebug()<<"搜狗搜索请求成功"; // QFile file("./test.html"); // if(file.open(QIODevice::WriteOnly)){ // file.write(bt); // file.close(); // } //解析相应数据 data-url="https://baike.sogou.com/ QString result(bt); //转化为字符串 //提取 int idx = result.indexOf( QRegExp("data-url=\"https*://baike.sogou.com") ); QString str = result.mid( idx ); int beginIdx = str.indexOf(QRegExp("https*://") ); int endIdx = str.indexOf("\">"); str = str.mid( beginIdx , endIdx - beginIdx ); qDebug() << str; //发送第二个链接请求 m_request2->sendGetRequest(str); } else{ qDebug()<<"搜狗搜索请求失败"; } }
(四)提取想要的内容到界面
void MainWindow::slot_getBaikeRes(int code, QByteArray &bt) { if(code>0){ qDebug()<<"百科请求成功"; //解析,提取想要的内容到界面 QString result(bt); //转化为字符串 //提取 j-shareAbstract int idx = result.indexOf( QRegExp("<div id=\"j-shareAbstract\" style=\"display:none\">") ); QString str = result.mid( idx ); int beginIdx = QString("<div id=\"j-shareAbstract\" style=\"display:none\">").length(); int endIdx = str.indexOf("</div>"); str = str.mid( beginIdx , endIdx - beginIdx ); qDebug() << str; ui->te_content->setText(str); } else{ qDebug()<<"百科请求失败"; } }
邮箱验证
(一)获取QQ邮箱授权码
[获取QQ邮箱授权码] https://blog.csdn.net/xiaowei1028/article/details/118305788
(二)导入MailApi
(三)服务器
CSmtpMail m_mai; std::map<std::string,int> m_mapMailToCode; { //服务器收到信息 //服务器 生成一个随机验证码,同时将用户信息和验证码绑定,map int code=123678; m_mapMailToCode[mail.toStdString()]=code; //map中key为邮箱,value为验证码 //发送邮件 通过 SMTP协议,利用QQ邮箱发送 m_mai.SendMail(mail.toStdString(),to_string(code)); } { //服务器:根据信息,查map,找到对应验证码,比对:一致,返回验证成功 //服务器解析 if(m_mapMailToCode[mail.toStdString()]>0){ int code=m_mapMailToCode[mail.toStdString()]; if(recvCode.toInt()==code){ QMessageBox::about(this,"info","验证成功"); } else{ QMessageBox::about(this,"验证失败","验证码错误"); } } else{ QMessageBox::about(this,"tip","未知邮箱"); } }
(四)客户端
void Dialog::on_pb_getcode_clicked() //获取验证码 { //获取信息 QString tel=ui->lb_tel->text(); QString mail=ui->le_mail->text(); //过滤todo //手机和邮箱发送到服务器 } void Dialog::on_pb_commit_clicked() { //用户将邮件收到的验证码填入界面 QString mail=ui->le_mail->text(); QString recvCode=ui->le_code->text(); //提交验证码和信息 }
SMTP的架构与连接
SMTP架构
SMTP端口与服务器
25端口
SMTP连接方式
1)建立会话EHELO
WSADATA WSAData; struct sockaddr_in their_addr = { 0 }; WSAStartup(MAKEWORD(2, 2), &WSAData); memset(&their_addr, 0, sizeof(their_addr)); their_addr.sin_family = AF_INET; their_addr.sin_port = htons(m_serverPort); // 一般是25端口不需要改 hostent* hptr = gethostbyname(m_smtpServer.c_str()); // 端口和服务器 memcpy(&their_addr.sin_addr.S_un.S_addr, hptr->h_addr_list[0], hptr->h_length); printf("IP of %s is : %d:%d:%d:%d\n", m_smtpServer.c_str(), their_addr.sin_addr.S_un.S_un_b.s_b1, their_addr.sin_addr.S_un.S_un_b.s_b2, their_addr.sin_addr.S_un.S_un_b.s_b3, their_addr.sin_addr.S_un.S_un_b.s_b4); // 连接邮件服务器,如果连接后没有响应,则2ms后重新连接 sockfd = ConnectServer((struct sockaddr *)&their_addr); memset(rbuf, 0, 1500); while (recv(sockfd, rbuf, 1500, 0) == 0) { cout << "reconnect..." << endl; Sleep(2); sockfd = ConnectServer((struct sockaddr *)&their_addr); memset(rbuf, 0, 1500); } cout << rbuf << endl; // EHLO sprintf_s(buf, 1500, "EHLO HYL-PC\r\n"); send(sockfd, buf, strlen(buf), 0); memset(rbuf, 0, 1500); recv(sockfd, rbuf, 1500, 0); cout << "EHLO REceive: " << rbuf << endl;
2)身份认证AUTH
// AUTH LOGIN sprintf_s(buf, 1500, "AUTH LOGIN\r\n"); send(sockfd, buf, strlen(buf), 0); memset(rbuf, 0, 1500); recv(sockfd, rbuf, 1500, 0); cout << "Auth Login Receive: " << rbuf << endl; // USER sprintf_s(buf, 1500, m_emailSenderAddr.c_str());//你的邮箱账号 memset(login, 0, 128); EncodeBase64(login, buf, strlen(buf)); //base64编码 sprintf_s(buf, 1500, "%s\r\n", login); send(sockfd, buf, strlen(buf), 0); cout << "Base64 UserName: " << buf << endl; memset(rbuf, 0, 1500); recv(sockfd, rbuf, 1500, 0); cout << "User Login Receive: " << rbuf << endl; // PASSWORD sprintf_s(buf, 1500, m_emailSenderPasswd.c_str());//你的邮箱密码 授权码 memset(pass, 0, 128); EncodeBase64(pass, buf, strlen(buf)); sprintf_s(buf, 1500, "%s\r\n", pass); send(sockfd, buf, strlen(buf), 0); cout << "Base64 Password: " << buf << endl; memset(rbuf, 0, 1500); recv(sockfd, rbuf, 1500, 0); cout << "Send Password Receive: " << rbuf << endl;
3)发送邮件信封MAIL FROM、RCPT TO
// MAIL FROM sprintf_s(buf, 1500, "MAIL FROM: <%s>\r\n",m_emailSenderAddr.c_str()); //此处要和发邮件的邮箱保持一致 send(sockfd, buf, strlen(buf), 0); memset(rbuf, 0, 1500); recv(sockfd, rbuf, 1500, 0); cout << "set Mail From Receive: " << rbuf << endl; // RCPT TO 第一个收件人 sprintf_s(buf, 1500, "RCPT TO:<%s>\r\n", m_emailReceiverAddr.c_str() ); send(sockfd, buf, strlen(buf), 0); memset(rbuf, 0, 1500); recv(sockfd, rbuf, 1500, 0); cout << "Tell Sendto Receive: " << rbuf << endl;
4)发送邮件内容DATA
// DATA 准备开始发送邮件内容 sprintf_s(buf, 1500, "DATA\r\n"); send(sockfd, buf, strlen(buf), 0); memset(rbuf, 0, 1500); recv(sockfd, rbuf, 1500, 0); cout << "Send Mail Prepare Receive: " << rbuf << endl; // 发送邮件内容,\r\n.\r\n内容结束标记 sprintf_s(buf, 1500, "%s\r\n.\r\n", body); send(sockfd, buf, strlen(buf), 0); memset(rbuf, 0, 1500); recv(sockfd, rbuf, 1500, 0); cout << "Send Mail Receive: " << rbuf << endl;
5)关闭会话QUIT
// QUIT sprintf_s(buf, 1500, "QUIT\r\n"); send(sockfd, buf, strlen(buf), 0); memset(rbuf, 0, 1500); recv(sockfd, rbuf, 1500, 0); cout << "Quit Receive: " << rbuf << endl;
每个邮件就是一个连接(短连接)
JSON实现
Java Script Object Notation,js对象表示法。是一对一对键值对形成的字符串
JSON是一种数据封装,可以用于网络传输
{ "key":"value", "hobby":["唱","跳","rap","篮球"], "data":[ { "name":"张三", "age":15 }, { "name":"李四", "age":18 }, ], "status":true, "num":null } key值只能是字符串 value值可以是bool、数组(多个JSON对象,或多个字符串)、字符串、整型、JSON对象、空值
QT可以使用QT自带的JSON去实现,linux可以使用开源的json.c
JSON相比结构体的优势?
-
结构体有字节对齐问题,在不同的环境下可能会不同(pc、web、C、java)
-
结构体无法对空值进行描述,JSON可以避免一些异常情况(null 0)
JSON使用需要注意的事情:
-
会传输多余的数据
-
传输文件内容、音频视频等会带有0的数据,会产生问题(字符串遇到\0结束)
对有0的数据,可以通过base64编码
http请求,protobuf
添加共享库:
打开共享库路径配置文件
sudo vi /etc/ld.so.conf
最后一行添加lib路径
更新共享库加载路径
sudo ldconfig -v
网络抓包 异常处理
wireshark
[wireshark抓包及常用协议分析] https://blog.csdn.net/qq_30036471/article/details/102776661
异常处理
遇到问题,返回错误码
try{ if(...) throw std::runtime_error("xxx error"); }catch(std::exception &exp){ cout<<exp.what()<<endl; //查看错误打印 }
这样就可以在遇到问题时,不用跳出程序也能得到错误信息
class MyException:public std::exception{ public: MyException(const std::string &str):m_str(str){} virtual ~MyException(){} virtual const char* what() const noexcept{ return m_str.c_str(); } std::string m_str; }; void Dialog::on_pb_equal_clicked() { int num1=ui->le_1->text().toInt(); int num2=ui->le_2->text().toInt(); int res=0; try { if(num2==0){ //throw std::runtime_error("除数不能为0"); throw MyException("除数不能为0"); } res=num1/num2; ui->le_res->setText(QString::number(res)); } catch (std::exception& exp) { qDebug()<<exp.what(); } qDebug()<<"quit"; }
断言
assert(); //断言,参数是一个判断语句。如果判断为假,会中止程序,弹出弹窗提示错误
int num1=ui->le_1->text().toInt(); int num2=ui->le_2->text().toInt(); assert(num2!=0); //为假,弹窗中止程序 qDebug()<<"ignore"; int res=num1/num2; ui->le_res->setText(QString::number(res));
异常处理与使用错误码有什么区别?
-
异常处理更专业,可以简化代码流程,便于开发
-
异常处理可以嵌套,通过另一个函数throw错误,简化代码流程
平时我们的代码是通过返回错误码以及errno配合使用,代码较复杂
int call(int num1,int num2){ int res=0; if(num2==0){ throw MyException("除数不能为0"); } res=num1/num2; return res; } { int num1=ui->le_1->text().toInt(); int num2=ui->le_2->text().toInt(); int res=0; try{ res=call(num1,num2); ui->le_res->setText(QString::number(res)); }catch (std::exception& exp) { qDebug()<<exp.what(); } }
内存泄漏
导致内存泄漏的方式
-
申请的堆空间没有主动回收
-
打开的文件没有关闭
-
打开的网络连接没有close()
-
句柄没有关闭
-
资源未关闭,包括ffmpeg库 SDL库 网络库 等没有关闭
-
线程没有退出
内存泄漏检测
valgrind
sudo apt-get install valgrind g++ -std=c++11 xx.cpp -g -o app valgrind --leak-check=full --show-leak-kinds=all ./app
#include <iostream> #include <stdlib.h> #include <stdio.h> using namespace std; int main(){ int *p=new int[10]; FILE* pFile=fopen("test.txt","w"); fwrite("hello",1,5,pFile); }
核心转储
linux下core dump文件,在发生核心转储时,gdb调试可以把段错误存储在核心错误文件
#include <iostream> using namespace std; int main(){ int* p =nullptr; *p=10; }
g++ -std=gnu++11 xx.cpp -g -o app gdb ./app #当发生段错误,可以使用命令将错误保存在文件中 gcore #或:generate-core-file #退出后,可以查看保存的文件 gdb -q ./app ./core.线程号
在当前目录生成核心转储文件
ulimit -c unlimited #发生段错误会生成一个core文件
加解密
IM及时通讯软件,聊天的内容需要加密
不加密,任何人都可以拦截
如何加密?
密码:MD5(信息摘要算法),将明文转换为密文(不可逆),通过哈希算法,转换为128的二进制数,平时使用32位16进制的字符串来表示。写入数据库中,即使数据库被攻破,也不会立即直到内容是什么。但是可以利用截获的包,直接登录
HTTPS加密流程
1. AES对称加密
双方使用一个约定好的随机生成的密钥,后续的通信中,信息发送方都使用密钥对信息加密,而信息接收方通过同样的密钥对信息解密。
但是只要密钥泄漏或被破解,中间人就能解密通信了
2. RAS非对称加密
明文既可以用公钥加密,用私钥解密;也可以用私钥加密,用公钥解密。
一般来说,双方公开双方的公钥,使用对方的公钥进行明文加密,收到信息后用自己的私钥进行解密,就可以安全的进行信息通信。但是RSA的过程十分的缓慢,所以还是要用对称进行通信
方法:
A发布公钥A,B收到公钥A后,生成一个用于对称加密的密钥key,用公钥A对密钥key加密,将密文发给A。A收到密文后,用私钥A解密,就能得到密钥key。这样双方就可以用密钥key进行对称加密通信了
但是如果中间人把公钥A,换成了自己的公钥C,发给B。B发回消息就能被中间人用私钥C解密,获取密钥key,然后再用公钥A加密伪装发给A。这样双方都不知情的情况下,中间人得到了密钥key
3.CA机构(权威的证书颁发机构)
比对两次前面是否一致,一致证明没有发生篡改。证书是三方机构保证的,假证书无法通过验证
1.作为服务端的小红,首先把自己的公钥发给证书颁发机构,向证书颁发机构申请证书
2.证书颁发机构自己也有一对公钥私钥。机构利用自己的私钥来加密Key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样经过机构的私钥加密。证书制作完成后,机构把证书发送给了服务端小红
3.当小灰向小红请求通信的时候,小红不再直接返回自己的公钥,而是把自己申请的证书返回给小灰。
4.小灰收到证书以后,要做的第一件事情是验证证书的真伪。需要说明的是,各大浏览器和操作系统已经维护了所有权威证书机构的名称和公钥。所以小灰只需要知道是哪个机构颁布的证书,就可以从本地找到对应的机构公钥,解密出证书签名。
接下来,小灰按照同样的签名规则,自己也生成一个证书签名,如果两个签名一致,说明证书是有效的。
验证成功后,小灰就可以放心地再次利用机构公钥,解密出服务端小红的公钥Key1
5.像之前一样,小灰生成自己的对称加密密钥Key2,并且用服务端公钥Key1加密Key2,发送给小红。
6.最后,小红用自己的私钥解开加密,得到对称加密密钥Key2。于是两人开始用Key2进行对称加密的通信。
SSL/TLS 协议
C/S下的加解密
C/S 和 B/S架构不同:
-
可以通过官网下载APP,有文件的验证(MD5 或 SHA,信息一致性验证),确保APP一定是官方的
-
程序运行时,可以携带数据。CA证书保证客户端收到的服务器公钥一定来自服务器,但是只要通过程序运行携带数据,就可以直接获取公钥,而不需要网络传输获取
C/S架构加密,不需要CA,直接从步骤5开始,完成加密流程:
-
通过随机数,生成AES加密的密钥key
-
通过服务器公钥,key加密后传给服务器
-
服务器通过非对称的私钥解密,得到AES加密的密钥key
-
此时,客户端服务器就可以通过AES密钥完成加密通信了
为什么还需要非对称加密?
AES是随机固定的。若AES是动态的,例如通过时间戳换算得到,这样的密钥是可以通过反编译破解的。需要非对称把密文保护起来。此时服务器的公钥和私钥是固定的,想要破解需要花费很多时间,此时可以定期更新服务器的RSA密钥对,通过版本更新变换,旧版本软件作废。
代码实现
使用了cryptopp开源的加解密库
windows下有编译好的,5.12.11版本mingw32,能直接包含.pri使用
linux下
让共享库生效